diff --git a/.github/workflows/apidocs.yml b/.github/workflows/apidocs.yml index 4f8b6731a..87da78b75 100644 --- a/.github/workflows/apidocs.yml +++ b/.github/workflows/apidocs.yml @@ -30,11 +30,6 @@ jobs: source .venv/bin/activate python3 resources/scripts/apidocs.py - - name: Run graphdocs script - run: | - source .venv/bin/activate - python3 resources/scripts/graphdocs.py - - uses: stefanzweifel/git-auto-commit-action@v5 with: - commit_message: Update apidocs and/or graphdocs + commit_message: Update apidocs on ${{ github.head_ref }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 021b70d66..000000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: deploy -on: - workflow_run: - workflows: ["test"] - types: - - completed - -jobs: - deploy-to-dockerhub: - runs-on: ubuntu-latest - if: github.event.workflow_run.conclusion == 'success' - steps: - - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Docker meta - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_RPC_REPO }} - tags: | - type=ref,event=tag - type=ref,event=pr - type=raw,value=latest,enable={{is_default_branch}} - labels: | - maintainer=bitcoindevproject - org.opencontainers.image.title=warnet-rpc - org.opencontainers.image.description=Warnet RPC server - - name: Login to Docker Hub - if: github.ref == 'refs/heads/main' - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build production RPC image - uses: docker/build-push-action@v5 - with: - file: resources/images/rpc/Dockerfile_prod - platforms: linux/amd64,linux/arm64 - context: . - push: ${{ github.ref == 'refs/heads/main' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - name: Build dev RPC image - uses: docker/build-push-action@v5 - with: - file: resources/images/rpc/Dockerfile_dev - platforms: linux/amd64,linux/arm64 - context: resources/images/rpc - push: ${{ github.ref == 'refs/heads/main' }} - tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_RPC_REPO }}:dev - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/publish-dist.yml b/.github/workflows/publish-dist.yml index d228464e0..b0dad4cf7 100644 --- a/.github/workflows/publish-dist.yml +++ b/.github/workflows/publish-dist.yml @@ -1,4 +1,4 @@ -name: Publish Python 🐍 distribution 📦 to PyPI +name: Publish 🐍 📦 to PyPI on: push @@ -6,13 +6,12 @@ jobs: build: name: Build distribution 📦 runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.x" + python-version: '3.12' - name: Install pypa/build run: >- python3 -m @@ -22,7 +21,7 @@ jobs: - name: Build a binary wheel and a source tarball run: python3 -m build - name: Store the distribution packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ @@ -42,7 +41,7 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ @@ -63,12 +62,12 @@ jobs: steps: - name: Download all the dists - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Sign the dists with Sigstore - uses: sigstore/gh-action-sigstore-python@v2.1.1 + uses: sigstore/gh-action-sigstore-python@v3.0.0 with: inputs: >- ./dist/*.tar.gz diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0524bed9c..e8a5a5573 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,108 +4,98 @@ on: pull_request: push: branches: - - main + - '**' + +env: + PYTHON_VERSION: "3.12" + STERN_VERSION: "1.30.0" jobs: - # ruff: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # - uses: chartboost/ruff-action@v1 - # with: - # args: 'check .' - ruff-format: + + ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: chartboost/ruff-action@v1 + - name: Install uv + uses: astral-sh/setup-uv@v5 with: - args: 'format --check .' - build-image: - # needs: [ruff, ruff-format] - needs: [ruff-format] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and export - uses: docker/build-push-action@v5 + version: "0.6.11" + enable-cache: true + - name: Lint + uses: astral-sh/ruff-action@v3 with: - file: resources/images/rpc/Dockerfile_prod - context: . - tags: warnet/dev - cache-from: type=gha - cache-to: type=gha,mode=max - outputs: type=docker,dest=/tmp/warnet.tar - - - name: Upload artifact - uses: actions/upload-artifact@v4 + args: "check --fix" + - name: Format + uses: astral-sh/ruff-action@v3 with: - name: warnet - path: /tmp/warnet.tar + args: "format --check --diff" + test: - needs: [build-image] + needs: [ruff] runs-on: ubuntu-latest strategy: matrix: - test: [scenarios_test.py, rpc_test.py, graph_test.py, ln_test.py, dag_connection_test.py, logging_test.py] + test: + - bitcoin_rpc_args_test.py + - conf_test.py + - dag_connection_test.py + - graph_test.py + - logging_test.py + - ln_basic_test.py + - ln_test.py + - onion_test.py + - plugin_test.py + - rpc_test.py + - services_test.py + - signet_test.py + - scenarios_test.py + - namespace_admin_test.py + - wargames_test.py steps: - uses: actions/checkout@v4 - - uses: hynek/setup-cached-uv@v1 - uses: azure/setup-helm@v4.2.0 - - uses: medyagh/setup-minikube@master + - name: start minikube + uses: medyagh/setup-minikube@v0.0.20 + id: minikube with: - mount-path: ${{ github.workspace }}:/mnt/src - - uses: actions/download-artifact@v4 + minikube-version: 1.37.0 + kubernetes-version: v1.34.0 + cpus: max + memory: 4000m + - name: Start minikube's loadbalancer tunnel + run: minikube tunnel &> /dev/null & + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v5 with: - name: warnet - path: /tmp - - run: | - echo loading the image directly into minikube docker - eval $(minikube -p minikube docker-env) - docker load --input /tmp/warnet.tar - docker image ls -a - - echo Installing warnet python package for cli - uv venv - uv pip install -e . - - echo "Contents of warnet-rpc-statefulset-dev.yaml being used:" - cat resources/manifests/warnet-rpc-statefulset-dev.yaml - - echo Setting up k8s - kubectl apply -f resources/manifests/namespace.yaml - kubectl apply -f resources/manifests/rbac-config.yaml - kubectl apply -f resources/manifests/warnet-rpc-service.yaml - kubectl apply -f resources/manifests/warnet-rpc-statefulset-dev.yaml - kubectl config set-context --current --namespace=warnet - - echo sleeping for 30s to give k8s time to boot - sleep 30 - kubectl describe pod rpc-0 - kubectl logs rpc-0 - - echo Waiting for rpc-0 to come online - until kubectl get pod rpc-0 --namespace=warnet; do - echo "Waiting for server to find pod rpc-0..." - sleep 4 - done - kubectl wait --for=condition=Ready --timeout=2m pod rpc-0 - shell: bash - - run: | - kubectl port-forward svc/rpc 9276:9276 & + version: "latest" + enable-cache: true + - name: "Set up Python" + uses: actions/setup-python@v5 + with: + python-version-file: "pyproject.toml" + - name: Install project + run: uv sync --all-extras --dev - name: Run tests run: | source .venv/bin/activate ./test/${{matrix.test}} - # build-test: - # needs: [build-image] - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # - uses: ./.github/actions/compose - # - run: ./test/build_branch_test.py compose + - name: Collect Kubernetes logs + if: always() + run: | + echo "Installing stern..." + curl -Lo stern.tar.gz https://github.com/stern/stern/releases/download/v${STERN_VERSION}/stern_${STERN_VERSION}_linux_amd64.tar.gz + tar zxvf stern.tar.gz + chmod +x stern + sudo mv stern /usr/local/bin/ + + # Run script + curl -O https://raw.githubusercontent.com/bitcoin-dev-project/warnet/main/resources/scripts/k8s-log-collector.sh + chmod +x k8s-log-collector.sh + ./k8s-log-collector.sh default + - name: Upload log artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: kubernetes-logs-${{ matrix.test }} + path: ./k8s-logs + retention-days: 5 diff --git a/.gitignore b/.gitignore index c42f6ba7f..35e704c67 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ warnet.egg-info .env dist/ build/ +**/kubeconfigs/ +src/warnet/_version.py diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 000000000..e06b03bfd --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,176 @@ +# Overview + +Warnet is designed for Bitcoin developers, researchers, and enthusiasts who want to safely experiment with and test ideas related to Bitcoin networks. By leveraging Kubernetes, Warnet enables users to deploy and simulate large-scale Bitcoin networks in a controlled and reproducible environment. + +## Table of Contents + +1. [Overview](#overview) + - [Key Features](#key-features) + - [Target Audience](#target-audience) + - [Benefits](#benefits) + +2. [Philosophy](#philosophy) + +3. [Kubernetes and Helm in Warnet](#kubernetes-and-helm-in-warnet) + - [Kubernetes](#kubernetes) + - [Helm](#helm) + - [Why Helm is Preferred in Warnet](#why-helm-is-preferred-in-warnet) + +4. [Project Structure](#project-structure) + - [Overview of Resources](#overview-of-resources) + - [Overview of src/warnet](#overview-of-srcwarnet) + - [Overview of Test](#overview-of-test) + +5. [Operating in the Network with "Scenarios"/Commanders](#operating-in-the-network-with-scenarioscommanders) + +6. [The Resources Configuration Pipeline - An Example](#the-resources-configuration-pipeline---an-example) + +## Key Features: +- Safe simulation of Bitcoin networks +- Kubernetes-based deployment for scalability and ease of management +- User interaction capabilities through custom scenarios +- Infrastructure as Code (IaC) approach for reproducibility + +## Target Audience: +- Bitcoin core developers +- Bitcoin researchers +- Network security specialists +- Bitcoin protocol testers +- Bitcoin application developers +- Lightning developers +- Students of bitcoin +- And more! + +## Benefits: +1. Scalability: Simulate networks of various sizes to understand behaviour at scale +2. Reproducibility: Easily recreate test environments using IaC principles +3. Flexibility: Customize network configurations and scenarios to test specific hypotheses +4. Risk-free experimentation: Test new ideas without affecting real Bitcoin networks +5. Educational: Learn about Bitcoin network behaviour in a controlled setting + +## Philosophy + +The implementation should follow a native Kubernetes (via Helm) approach wherever possible (see [Kubernetes and Helm in Warnet](#kubernetes-and-helm-in-warnet) for more information on these applications). This simplifies development in this project, and offloads responsibility over (often) non-trivial components to the Kubernetes development team. Ideologically this means that all Warnet components should be thought of as "Kubernetes resources which are to be controlled (installed/uninstalled) using Helm", wherever possible. + +This philosophy applies to the implementation in this codebase: there are often multiple possible ways of implementing a new feature and developers should first seek out the way this is usually achieved on Kubernetes (natively) and encode this in a Helm chart, before falling back to a custom solution if this is not viable. + +For example, a new logging component could be added in multiple possible ways to Warnet: + +1. Run a process on the local host which connects in to the cluster via a forwarded port and performs customised logging. + +2. Launch a standardised logging application (e.g. Grafana) via executing a `kubectl` command on a *.yaml* file. + + ```python + subprocess.run("kubectl apply -f grafana.yaml --namespace=grafana") + ``` + +3. Create a helm chart (or use an [already-available](https://github.com/grafana/helm-charts) one) for the Grafana component and install using helm. + + ```python + subprocess.run("helm upgrade --install --namespace logging promtail grafana/promtail") + ``` + +Out of these three options, the third should be preferred where possible, followed by the second, with the first only being used in extreme cases. + +## Kubernetes and Helm in Warnet + +
+ Kubernetes logo + Helm logo +
+ +Warnet leverages [Kubernetes](https://kubernetes.io/) for deploying and managing simulated Bitcoin networks, with [Helm](https://helm.sh/) serving as the preferred method for managing Kubernetes resources. Understanding the relationship between these technologies is helpful for grasping Warnet's architecture and deployment strategy. + +### Kubernetes +[Kubernetes](https://kubernetes.io/) is an open-source container orchestration platform that automates the deployment, scaling, and management of containerized applications. In Warnet, Kubernetes provides the underlying infrastructure for running simulated Bitcoin nodes and related services. + +Key benefits of using Kubernetes in Warnet: +- Scalability: Easily scale the number of simulated nodes +- Resource management: Efficiently allocate computational resources +- Service discovery: Automatically manage network connections between nodes + +### Helm +[Helm](https://helm.sh/) is a package manager for Kubernetes that simplifies the process of defining, installing, and upgrading complex Kubernetes applications. Warnet prefers Helm for managing Kubernetes resources due to its powerful templating and package management capabilities. + +Advantages of using Helm in Warnet: +- Templating: Define reusable Kubernetes resource templates +- Packaging: Bundle related Kubernetes resources into a single unit (chart) +- Simplified deployment: Use a single command to deploy complex applications + +### Why Helm is Preferred in Warnet +1. Reproducibility: Helm charts ensure consistent deployments across different environments. +2. Customization: Users can easily modify network configurations by adjusting Helm chart values. +3. Modularity: New components can be added to Warnet by creating or integrating existing Helm charts. +4. Simplified management: Helm's release management makes it easy to install, upgrade, rollback, or delete entire simulated networks. + +By leveraging Kubernetes with Helm, Warnet achieves a flexible, scalable, and easily manageable architecture for simulating Bitcoin networks. This approach aligns with the project's philosophy of using native Kubernetes solutions and following best practices in cloud-native application deployment. + +## Project structure +The Warnet code base has four main sections: + +1. *resources* - these items are available during runtime and relate to configuration (crucially, Kubernetes configuration) +2. *src/warnet* - python source code lives here +3. *test* - CI testing files live here +4. *docs* - stores documentation available in the github repository + +### Overview of resources +There are four main kinds of *resources*: + +1. Kubernetes configuration files - they are the backbone of Stateless Configuration; they are *yaml* files. + > [!NOTE] + > Whilst native Kubernetes *yaml* configuration files can and do exist here, Helm charts are the preferred way to configure Kubernetes resources. +2. scenarios - these are python programs that users can load into the cluster to interact with the simulated Bitcoin network +3. images - the logic for creating bitcoin nodes and also containers for running scenarios are found here; this includes Dockerfiles +4. scripts and other configs - these are like "assets" or "one off" items which appear in Warnet. + +### Overview of src/warnet +The python source code found in *src/warnet* serves to give users a way to create and interact with the simulated Bitcoin network as it exists in a Kubernetes cluster. + +There are eight categories of python program files in Warnet: + +1. Bitcoin images + * *image.py* and *image_build.py* - the logic that helps the user create bitcoin node images +2. Bitcoin interaction + * *bitcoin.py* - make it easy to interact with bitcoin nodes in the simulated network +3. Scenario interaction + * *control.py* - launch scenarios in order to interact with the simulated Bitcoin network +4. Kubernetes + * *k8s.py* - gather Kubernetes configuration data; retrieve Kubernetes resources + * *status.py* - make it easy for the user to see the status of the simulated bitcoin network +5. Resource configuration pipeline + * *admin.py* - copy configurations for *resources* such as namespaces and put them in the user's directory + * *deploy.py* - take configurations for *resources* and put them into the Kubernetes cluster + * *network.py* - copy *resources* to the users Warnet directory + * *namespaces.py* - copy *resources* to the users Warnet directory; interact with namespaces in the cluster +6. User interaction + * *main.py* - provide the interface for the `warnet` command line program +7. Host computer + * *process.py* - provides a way to run commands on the user's host computer +8. Externalized configuration + * *constants.py* - this holds values which occur repeatedly in the code base + +### Overview of test +The *test_base.py* file forms the basis of the *test* section. Each test uses *TestBase* which controls the test running framework. + +### Operating in the network with "scenarios"/Commanders +Warnet includes the capability to run "scenarios" on the network. These are python files which can be found in *resources/scenarios*, or copied by default into a new project directory. + +These scenarios use a base class called `Commander` which ultimately inherits from the Bitcoin Test Framework. This means that scenarios can be written and controlled using the familiar Bitcoin Core functional test language. The `self.nodes[index]` property has been patched to automatically direct commands to a running bitcoin node of the same index in the network. + +Once a scenario has been written, it can be loaded into the cluster and run using `warnet run `. + +### The resources configuration pipeline - an example +It is important to focus on the pipeline that takes *resources*, copies them into user directories, and translates them into Kubernetes objects. To make this possible and to achieve a more stateless configuration, Warnet uses Helm which provides templating for Kubernetes configuration files. + +Looking more closely at the *resources/charts* section, for example, we can focus in on the *bitcoincore* directory. Inside, there is an example *namespaces.yaml* and *namespace-defaults.yaml* file. These configuration files are provided to the user in their project directory when the `warnet init` command is invoked. + +This provides the user the opportunity to change those configuration files and modify both configuration defaults for all nodes, along with specific node settings. When `warnet deploy [project-dir]` command is run by the user, it will apply the configuration data to the Helm chart found in the *charts* directory of the *resources* section. The Helm chart acts as a template through which the user's configuration data is applied. + +In this way, there is a pipeline which starts with the user's Stateful Data which is then piped through the Helm templating system, and then is applied to the Kubernetes cluster. + +> [!TIP] +> Along with the python source code, all resources found in the *resources* directory are also included in the `warnet` python package. +> In this way default resources such as helm charts are available to the CLI application via the `importlib.resources` module. + +> [!TIP] +> To learn more about the resources configuration pipeline used in Warnet see the [configuration](docs/config.md) overview. diff --git a/README.md b/README.md index ff5b7ebb6..787c28f44 100644 --- a/README.md +++ b/README.md @@ -5,25 +5,96 @@ Monitor and analyze the emergent behaviors of Bitcoin networks. ## Major Features -* Launch a bitcoin network with a specified number of nodes connected to each other according to a network topology from a graphml file. -* Scenarios can be run across the network which can be programmed using the Bitcoin Core functional [test_framework language](https://github.com/bitcoin/bitcoin/tree/master/test/functional). -* Nodes can have traffic shaping parameters assigned to them via the graph using [tc-netem](https://manpages.ubuntu.com/manpages/trusty/man8/tc-netem.8.html) tool. -* Data from nodes can be collected and searched including log files and p2p messages. -* Performance data from containers can be monitored and visualized. -* Lightning Network nodes can be deployed and operated. -* Networks can be deployed using Kubernetes, e.g. via MiniKube (small network graphs) or a managed cluster for larger network graphs. +* Launch a bitcoin network with a specified number of nodes connected to each other according to a network topology. +* Run scenarios of network behavior across the network which can be programmed using the Bitcoin Core functional [test_framework language](https://github.com/bitcoin/bitcoin/tree/master/test/functional). +* Collect and search data from nodes including log files and p2p messages. +* Monitor and visualize performance data from Bitcoin nodes. +* Connect to a large network running in a remote cluster, or a smaller network running locally. +* Add a Lightning Network with its own channel topology and payment activity. ## Documentation +- [Design](/DESIGN.md) - [Installation](/docs/install.md) -- [Quick Run](/docs/quickrun.md) -- [Running Warnet](/docs/running.md) -- [Network Topology](/docs/graph.md) -- [CLI Commands](/docs/warcli.md) +- [CLI Commands](/docs/warnet.md) +- [Network configuration with yaml files](/docs/config.md) +- [Plugins](/docs/plugins.md) - [Scenarios](/docs/scenarios.md) - [Monitoring](/docs/logging_monitoring.md) -- [Lightning Network](/docs/lightning.md) +- [Snapshots](/docs/snapshots.md) +- [Connecting to local nodes outside the cluster](/docs/connecting-local-nodes.md) - [Scaling](/docs/scaling.md) -- [Connecting to local nodes](https://github.com/bitcoin-dev-project/warnet/blob/main/docs/) +- [Contributing](/docs/developer-notes.md) + + +## Quick Start + +### 1. Create a python virtual environment + +```sh +python3 -m venv .venv +source .venv/bin/activate +``` + +### 2. Install Warnet + +```sh +pip install warnet +``` + +### 3. Set up dependencies + +Warnet will ask which back end you want to use, check that it is working, +and install additional client tools into the virtual environment. + +```sh +warnet setup +``` + +### 4. Create a project and network + +Warnet will create a new folder structure containing standard scenario and plugin +files, and prompt for details about a network topology to create. Topology details +include number of Bitcoin nodes, which release versions or custom images to deploy +and how many random graph connections to start each node with. + +```sh +warnet new /my/work/stuff/projectname +``` + +### 5. Deploy the network + +```sh +warnet deploy /my/work/stuff/projectname/networks/networkname +``` + +### 6. Run experiments + +For example, you can start mining blocks... + +```sh +warnet run /my/work/stuff/projectname/scenarios/miner_std.py +``` + +... and then observe network connectivity and statistics in your browser: + +```sh +warnet dashboard +``` + +### 7. Shut down the network + +```sh +warnet down +``` + +### 8. Customize + +Read the docs and learn how to write your own [scenarios](docs/scenarios.md) +or add [plugins](docs/plugins.md) to your network. [Configure](docs/config.md) individual nodes +in the network by editing the `network.yaml` file or configure +defaults for all nodes in the network by editing `node-defaults.yaml`. Once +your network is running use Warnet [CLI](docs/warnet.md) commands to interact with it. + ![warnet-art](https://raw.githubusercontent.com/bitcoin-dev-project/warnet/main/docs/machines.webp) diff --git a/contrib/README.md b/contrib/README.md new file mode 100644 index 000000000..af5c69d06 --- /dev/null +++ b/contrib/README.md @@ -0,0 +1,3 @@ +# Community Contributions + +Contains community-contributed resources for Warnet. These contributions are not officially supported or maintained. diff --git a/contrib/nix/flake.lock b/contrib/nix/flake.lock new file mode 100644 index 000000000..b6be34669 --- /dev/null +++ b/contrib/nix/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1725983898, + "narHash": "sha256-4b3A9zPpxAxLnkF9MawJNHDtOOl6ruL0r6Og1TEDGCE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1355a0cbfeac61d785b7183c0caaec1f97361b43", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/contrib/nix/flake.nix b/contrib/nix/flake.nix new file mode 100644 index 000000000..95df768d6 --- /dev/null +++ b/contrib/nix/flake.nix @@ -0,0 +1,42 @@ +{ + description = "Warnet python development environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + devShells.default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + python3 + python3Packages.pip + # K8 dependencies for local cluster deployment. + minikube + kubectl + kubernetes-helm + ]; + + # Install project dependencies and executable. + shellHook = '' + # Create a virtual environment if it doesn't exist. + if [ ! -d ".venv" ]; then + python -m venv .venv + fi + + # Activate the virtual environment. + source .venv/bin/activate + # Install the project in editable mode. + pip install -e . + + echo "WARNET DEVELOPMENT SHELL ENABLED" + ''; + }; + } + ); +} diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 000000000..93a51e33d --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,230 @@ +group "all" { + targets = [ + "bitcoin-28-1", + "bitcoin-27", + "bitcoin-26", + "v0-21-1", + "v0-20-0", + "v0-19-2", + "v0-17-0", + "v0-16-1", + "bitcoin-unknown-message", + "bitcoin-invalid-blocks", + "bitcoin-50-orphans", + "bitcoin-no-mp-trim", + "bitcoin-disabled-opcodes", + "bitcoin-5k-inv" + ] +} + +group "maintained" { + targets = [ + "bitcoin-28-1", + "bitcoin-27", + "bitcoin-26" + ] +} + +group "practice" { + targets = [ + "bitcoin-unknown-message", + "bitcoin-invalid-blocks", + "bitcoin-50-orphans", + "bitcoin-no-mp-trim", + "bitcoin-disabled-opcodes", + "bitcoin-5k-inv" + ] +} + +group "vulnerable" { + targets = [ + "v0-21-1", + "v0-20-0", + "v0-19-2", + "v0-17-0", + "v0-16-1", + ] +} + +target "maintained-base" { + context = "./resources/images/bitcoin" + args = { + REPO = "bitcoin/bitcoin" + BUILD_ARGS = "--disable-tests --without-gui --disable-bench --disable-fuzz-binary --enable-suppress-external-warnings" + } + platforms = ["linux/amd64", "linux/arm64", "linux/arm/v7"] +} + +target "cmake-base" { + inherits = ["maintained-base"] + dockerfile = "./Dockerfile.dev" + args = { + BUILD_ARGS = "-DBUILD_TESTS=OFF -DBUILD_GUI=OFF -DBUILD_BENCH=OFF -DBUILD_UTIL=ON -DBUILD_FUZZ_BINARY=OFF -DWITH_ZMQ=ON" + } +} + +target "autogen-base" { + inherits = ["maintained-base"] + dockerfile = "./Dockerfile" +} + +target "bitcoin-28-1" { + inherits = ["autogen-base"] + tags = ["bitcoindevproject/bitcoin:28.1"] + args = { + COMMIT_SHA = "32efe850438ef22e2de39e562af557872a402c31" + } +} + +target "bitcoin-27" { + inherits = ["autogen-base"] + tags = ["bitcoindevproject/bitcoin:27.2"] + args = { + COMMIT_SHA = "bf03c458e994abab9be85486ed8a6d8813313579" + } +} + +target "bitcoin-26" { + inherits = ["autogen-base"] + tags = ["bitcoindevproject/bitcoin:26.2"] + args = { + COMMIT_SHA = "7b7041019ba5e7df7bde1416aa6916414a04f3db" + } +} + +target "practice-base" { + dockerfile = "./Dockerfile" + context = "./resources/images/bitcoin/insecure" + contexts = { + bitcoin-src = "." + } + args = { + ALPINE_VERSION = "3.20" + BITCOIN_VERSION = "28.1.1" + EXTRA_PACKAGES = "sqlite-dev" + EXTRA_RUNTIME_PACKAGES = "" + REPO = "willcl-ark/bitcoin" + } + platforms = ["linux/amd64", "linux/armhf"] +} + +target "bitcoin-unknown-message" { + inherits = ["practice-base"] + tags = ["bitcoindevproject/bitcoin:99.0.0-unknown-message"] + args = { + COMMIT_SHA = "ae999611026e941eca5c0b61f22012c3b3f3d8dc" + } +} + +target "bitcoin-invalid-blocks" { + inherits = ["practice-base"] + tags = ["bitcoindevproject/bitcoin:98.0.0-invalid-blocks"] + args = { + COMMIT_SHA = "9713324368e5a966ec330389a533ae8ad7a0ea8f" + } +} + +target "bitcoin-50-orphans" { + inherits = ["practice-base"] + tags = ["bitcoindevproject/bitcoin:97.0.0-50-orphans"] + args = { + COMMIT_SHA = "cbcb308eb29621c0db3a105e1a1c1788fb0dab6b" + } +} + +target "bitcoin-no-mp-trim" { + inherits = ["practice-base"] + tags = ["bitcoindevproject/bitcoin:96.0.0-no-mp-trim"] + args = { + COMMIT_SHA = "a3a15a9a06dd541d1dafba068c00eedf07e1d5f8" + } +} + +target "bitcoin-disabled-opcodes" { + inherits = ["practice-base"] + tags = ["bitcoindevproject/bitcoin:95.0.0-disabled-opcodes"] + args = { + COMMIT_SHA = "5bdb8c52a8612cac9aa928c84a499dd701542b2a" + } +} + +target "bitcoin-5k-inv" { + inherits = ["practice-base"] + tags = ["bitcoindevproject/bitcoin:94.0.0-5k-inv"] + args = { + COMMIT_SHA = "e70e610e07eea3aeb0c49ae0bd9f4049ffc1b88c" + } +} + +target "CVE-base" { + dockerfile = "./Dockerfile" + context = "./resources/images/bitcoin/insecure" + contexts = { + bitcoin-src = "." + } + platforms = ["linux/amd64", "linux/armhf"] + args = { + REPO = "josibake/bitcoin" + } +} + +target "v0-16-1" { + inherits = ["CVE-base"] + tags = ["bitcoindevproject/bitcoin:0.16.1"] + args = { + ALPINE_VERSION = "3.7" + BITCOIN_VERSION = "0.16.1" + COMMIT_SHA = "dc94c00e58c60412a4e1a540abdf0b56093179e8" + EXTRA_PACKAGES = "protobuf-dev libressl-dev" + EXTRA_RUNTIME_PACKAGES = "boost boost-program_options libressl" + PRE_CONFIGURE_COMMANDS = "sed -i '/AC_PREREQ/a\\AR_FLAGS=cr' src/univalue/configure.ac && sed -i '/AX_PROG_CC_FOR_BUILD/a\\AR_FLAGS=cr' src/secp256k1/configure.ac && sed -i 's:sys/fcntl.h:fcntl.h:' src/compat.h" + } +} + +target "v0-17-0" { + inherits = ["CVE-base"] + tags = ["bitcoindevproject/bitcoin:0.17.0"] + args = { + ALPINE_VERSION = "3.9" + BITCOIN_VERSION = "0.17.0" + COMMIT_SHA = "f6b2db49a707e7ad433d958aee25ce561c66521a" + EXTRA_PACKAGES = "protobuf-dev libressl-dev" + EXTRA_RUNTIME_PACKAGES = "boost boost-program_options libressl sqlite-dev" + } +} + +target "v0-19-2" { + inherits = ["CVE-base"] + tags = ["bitcoindevproject/bitcoin:0.19.2"] + args = { + ALPINE_VERSION = "3.12.12" + BITCOIN_VERSION = "0.19.2" + COMMIT_SHA = "e20f83eb5466a7d68227af14a9d0cf66fb520ffc" + EXTRA_PACKAGES = "sqlite-dev libressl-dev" + EXTRA_RUNTIME_PACKAGES = "boost boost-program_options libressl sqlite-dev" + } +} + +target "v0-20-0" { + inherits = ["CVE-base"] + tags = ["bitcoindevproject/bitcoin:0.20.0"] + args = { + ALPINE_VERSION = "3.12.12" + BITCOIN_VERSION = "0.20.0" + COMMIT_SHA = "0bbff8feff0acf1693dfe41184d9a4fd52001d3f" + EXTRA_PACKAGES = "sqlite-dev miniupnpc-dev" + EXTRA_RUNTIME_PACKAGES = "boost-filesystem miniupnpc-dev sqlite-dev" + } +} + +target "v0-21-1" { + inherits = ["CVE-base"] + tags = ["bitcoindevproject/bitcoin:0.21.1"] + args = { + ALPINE_VERSION = "3.17" + BITCOIN_VERSION = "0.21.1" + COMMIT_SHA = "e0a22f14c15b4877ef6221f9ee2dfe510092d734" + EXTRA_PACKAGES = "sqlite-dev" + EXTRA_RUNTIME_PACKAGES = "boost-filesystem sqlite-dev" + } +} diff --git a/docs/admin.md b/docs/admin.md new file mode 100644 index 000000000..b888de3d8 --- /dev/null +++ b/docs/admin.md @@ -0,0 +1,69 @@ +# Admin + +## Connect to your cluster + +Ensure you are connected to your cluster because Warnet will use your current configuration to generate configurations for your users. + +```shell +$ warnet status +``` + +Observe that the output of the command matches your cluster. + +## Create an *admin* directory + +```shell +$ mkdir admin +$ cd admin +$ warnet admin init +``` + +Observe that there are now two folders within the *admin* directory: *namespaces* and *networks* + +## The *namespaces* directory +This directory contains a Helm chart named *two_namespaces_two_users*. + +Modify this chart based on the number of teams and users you have. + +Deploy the *two_namespaces_two_users* chart. + +```shell +$ warnet deploy namespaces/two_namespaces_two_users +``` + +Observe that this creates service accounts and namespaces in the cluster: + +```shell +$ kubectl get ns +$ kubectl get sa -A +``` + +### Creating Warnet invites +A Warnet invite is a Kubernetes config file. + +Create invites for each of your users. + +```shell +$ warnet admin create-kubeconfigs +``` + +Observe the *kubeconfigs* directory. It holds invites for each user. + +### Using Warnet invites +Users can connect to your wargame using their invite. + +```shell +$ warnet auth alice-wargames-red-team-kubeconfig +``` + +### Set up a network for your users +Before letting the users into your cluster, make sure to create a network of tanks for them to view. + + +```shell +$ warnet deploy networks/mynet --to-all-users +``` + +Observe that the *wargames-red-team* namespace now has tanks in it. + +**TODO**: What's the logging approach here? diff --git a/docs/circuit-breaker.md b/docs/circuit-breaker.md new file mode 100644 index 000000000..d6124c07d --- /dev/null +++ b/docs/circuit-breaker.md @@ -0,0 +1,180 @@ +# Circuit Breaker for Warnet + +## Overview + +Circuit Breaker is a Lightning Network firewall that protects LND nodes from being flooded with HTLCs. When integrated with Warnet, Circuit Breaker runs as a sidecar container alongside your LND nodes. + +Circuit Breaker is to Lightning what firewalls are to the internet - it allows nodes to protect themselves by setting maximum limits on in-flight HTLCs on a per-peer basis and applying rate limits to forwarded HTLCs. + +* **Repository**: https://github.com/lightningequipment/circuitbreaker +* **Full Documentation**: See the main repository for detailed information about Circuit Breaker's features, operating modes, and configuration options + +## Usage in Warnet + +### Basic Configuration + +To enable Circuit Breaker for an LND node in your `network.yaml` file, add the `circuitbreaker` section under the `lnd` configuration. When enabled, Circuit Breaker will automatically start as a sidecar container and connect to your LND node: + +```yaml +nodes: + - name: tank-0003 + addnode: + - tank-0000 + ln: + lnd: true + lnd: + config: | + bitcoin.timelockdelta=33 + channels: + - id: + block: 300 + index: 1 + target: tank-0004-ln + capacity: 100000 + push_amt: 50000 + circuitbreaker: + enabled: true # This enables Circuit Breaker for this node + httpPort: 9235 # Can override default port per-node (optional) +``` + +### Configuration Options + +- `enabled`: Set to `true` to enable Circuit Breaker for the node +- `httpPort`: Override the default HTTP port (9235) for the web UI (optional) + +### Complete Example + +Here's a complete `network.yaml` example with Circuit Breaker enabled on one node: + +```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: + config: | + bitcoin.timelockdelta=33 + channels: + - id: + block: 300 + index: 1 + target: tank-0004-ln + capacity: 100000 + push_amt: 50000 + circuitbreaker: + enabled: true + httpPort: 9235 + + - name: tank-0004 + addnode: + - tank-0000 + ln: + lnd: true + lnd: + channels: + - id: + block: 300 + index: 2 + target: tank-0005-ln + capacity: 50000 + push_amt: 25000 + + - name: tank-0005 + addnode: + - tank-0000 + ln: + lnd: true +``` + +## Accessing Circuit Breaker + +Circuit Breaker provides both a web-based interface and REST API endpoints for configuration and monitoring. + +### Web UI Access + +To access the web interface: + +1. **Port Forward to the Circuit Breaker service**: + ```bash + kubectl port-forward pod/-ln : + ``` + + For example, if your node is named `tank-0003` and using the default port: + ```bash + kubectl port-forward pod/tank-0003-ln 9235:9235 + ``` + +2. **Open your browser** and navigate to: + ``` + http://localhost:9235 + ``` + +3. **Configure your firewall rules** through the web interface: + - Set per-peer HTLC limits + - Configure rate limiting parameters + - Choose operating modes + - Monitor HTLC statistics + +### API Access + +You can also interact with Circuit Breaker programmatically using kubectl commands to access the REST API: + +**Get node information:** +```bash +kubectl exec -ln -c circuitbreaker -- wget -qO - 127.0.0.1:/api/info +``` + +**Get current limits:** +```bash +kubectl exec -ln -c circuitbreaker -- wget -qO - 127.0.0.1:/api/limits +``` + +For example, with node `tank-0003-ln`: +```bash +kubectl exec tank-0003-ln -c circuitbreaker -- wget -qO - 127.0.0.1:9235/api/info +kubectl exec tank-0003-ln -c circuitbreaker -- wget -qO - 127.0.0.1:9235/api/limits +``` + +## Architecture + +Circuit Breaker runs as a sidecar container alongside your LND node in Warnet: +- **LND Container**: Runs your Lightning node +- **Circuit Breaker Container**: Connects to LND via RPC and provides firewall functionality +- **Shared Volume**: Allows Circuit Breaker to access LND's TLS certificates and macaroons +- **Web Interface**: Accessible via port forwarding for configuration + +## Requirements + +- **LND Version**: 0.15.4-beta or above +- **Warnet**: Compatible with standard Warnet LND deployments + +## Support + +For issues and questions: +- Circuit Breaker Repository: https://github.com/lightningequipment/circuitbreaker +- Warnet Documentation: Refer to the Warnet installation guides [install.md](install.md) +- LND Documentation: https://docs.lightning.engineering/ + +--- + +*Circuit Breaker integration for Warnet enables sophisticated HTLC management and protection for Lightning Network nodes in test environments.* \ No newline at end of file diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 000000000..d87363766 --- /dev/null +++ b/docs/config.md @@ -0,0 +1,61 @@ +# Configuration value propagation + +This flowchart illustrates the process of how values for the Bitcoin Core module are handled and deployed using Helm in a Kubernetes environment. + +The process is similar for other modules (e.g. fork-observer), but may differ slightly in filenames. + +- The process starts with the `values.yaml` file, which contains default values for the Helm chart. +- There's a decision point to check if user-provided values are available. + These are found in the following files: + - For config applied to all nodes: `/node-defaults.yaml` + - For network and per-node config: `/network.yaml` + +> [!TIP] +> `values.yaml` can be overridden by `node-defaults.yaml` which can be overridden in turn by `network.yaml`. + +- If user-provided values exist, they override the defaults from `values.yaml`. If not, the default values are used. +- The resulting set of values (either default or overridden) becomes the final set of values used for deployment. +- These final values are then passed to the Helm templates. +- The templates (`configmap.yaml`, `service.yaml`, `servicemonitor.yaml`, and `pod.yaml`) use these values to generate the Kubernetes resource definitions. +- Helm renders these templates, substituting the values into the appropriate places. +- The rendering process produces the final Kubernetes manifest files. +- Helm then applies these rendered manifests to the Kubernetes cluster. +- Kubernetes processes these manifests and creates or updates the corresponding resources in the cluster. +- The process ends with the resources being deployed or updated in the Kubernetes cluster. + +In the flowchart below, boxes with a red outline represent default or user-supplied configuration files, blue signifies files operated on by Helm or Helm operations, and green by Kubernetes. + +```mermaid +graph TD + A[Start]:::start --> B[values.yaml]:::config + subgraph User Configuration [User configuration] + C[node-defaults.yaml]:::config + D[network.yaml]:::config + end + B --> C + C -- Bottom overrides top ---D + D --> F[Final values]:::config + F --> I[Templates]:::helm + I --> J[configmap.yaml]:::helm + I --> K[service.yaml]:::helm + I --> L[servicemonitor.yaml]:::helm + I --> M[pod.yaml]:::helm + J --> N[Helm renders templates]:::helm + K & L & M --> N + N --> O[Rendered kubernetes + manifests]:::helm + O --> P[Helm applies manifests to + kubernetes]:::helm + P --> Q["Kubernetes + creates/updates resources"]:::k8s + Q --> R["Resources + deployed/updated in cluster"]:::finish + + classDef start fill:#f9f,stroke:#333,stroke-width:4px + classDef finish fill:#bbf,stroke:#f66,stroke-width:2px,color:#fff,stroke-dasharray: 5 5 + classDef config stroke:#f00 + classDef k8s stroke:#0f0 + classDef helm stroke:#00f +``` + +Users should only concern themselves therefore with setting configuration in the `/[network|node-defaults].yaml` files. diff --git a/docs/connecting-local-nodes.md b/docs/connecting-local-nodes.md index 50455e704..89bade682 100644 --- a/docs/connecting-local-nodes.md +++ b/docs/connecting-local-nodes.md @@ -7,8 +7,7 @@ ### Run Warnet network ```shell -warcli cluster deploy -warcli network start +warnet deploy path/to/network/directory ``` ### Install Telepresence @@ -71,9 +70,9 @@ telepresence intercept local-bitcoind --port 18444 -- bitcoind --regtest --datad ### Connect to local bitcoind from cluster ```shell -warcli bitcoin rpc 0 addnode "local-bitcoind:18444" "onetry" +warnet bitcoin rpc 0 addnode "local-bitcoind:18444" "onetry" # Check that the local node was added -warcli bitcoin rpc 0 getpeerinfo +warnet bitcoin rpc 0 getpeerinfo ``` ### Disconnect and remove Telepresence @@ -85,4 +84,4 @@ telepresence quit -s telepresent helm uninstall # Remove Telepresence from your computer sudo rm /usr/local/bin/telepresence -``` \ No newline at end of file +``` diff --git a/docs/developer-notes.md b/docs/developer-notes.md index 47e22e812..3952a7a9c 100644 --- a/docs/developer-notes.md +++ b/docs/developer-notes.md @@ -1,21 +1,105 @@ # Developer notes -## Kubernetes +## Contributing / Local Warnet Development -Kubernetes is running the RPC server as a `statefulSet` which is pulled from a -container image on a registry. This means that (local) changes to the RPC -server are **not** reflected on the RPC server when running in Kubernetes, -unless you **also** push an updated image to a registry and update the -Kubernetes config files. +### Download the code repository -To help with this a helper script is provided: [build-k8s-rpc.sh](../scripts/build-k8s-rpc.sh). +```bash +git clone https://github.com/bitcoin-dev-project/warnet +cd warnet +``` + +### Recommended: use a virtual Python environment such as `venv` + +```bash +python3 -m venv .venv # Use alternative venv manager if desired +source .venv/bin/activate +``` + +```bash +pip install --upgrade pip +pip install -e . +``` + +## Formatting & linting + +This project primarily uses the `uv` python packaging tool: https://docs.astral.sh/uv/ along with the sister formatter/linter `ruff` https://docs.astral.sh/ruff/ -This script can be run in the following way: +Refer to the `uv` documentation for installation methods: https://docs.astral.sh/uv/getting-started/installation/ + +With `uv` installed you can add/remove dependencies using `uv add ` or `uv remove . +This will update the [`uv.lock`](https://docs.astral.sh/uv/guides/projects/#uvlock) file automatically. + +We use ruff version 0.11.0 in this project currently. This can be installed as a stand-alone binary (see documentation), or via `uv` using: ```bash -DOCKER_REGISTRY=bitcoindevproject/warnet-rpc TAG=0.1 ./scripts/build-k8s-rpc.sh Dockerfile_prod +# install +$ uv tool install ruff@0.11.0 + +# lint +$ uvx ruff@0.11.0 check . + +# format +$ uvx ruff@0.11.0 format . ``` -You can optionally specify `LATEST=1` to also include the `latest` tag on docker hub. +## Release process + +Once a tag is pushed to GH this will start an image build using the tag + +### Prerequisites + +- [ ] Update version in pyproject.toml +- [ ] Tag git with new version +- [ ] Push tag to GitHub + +### Manual Builds + +```bash +# Install build dependencies +pip install -e .[build] + +# Remove previous release metadata +rm -i -Rf build/ dist/ + +# Build wheel +python3 -m build +``` + +#### Upload + +```bash +# Upload to Pypi +python3 -m twine upload dist/* +``` + +## Building docker images + +The Bitcoin Core docker images used by warnet are specified in the *docker-bake.hcl* file. +This uses the (experimental) `bake` build functionality of docker buildx. +We use [HCL language](https://github.com/hashicorp/hcl) in the declaration file itself. +See the `bake` [documentation](https://docs.docker.com/build/bake/) for more information on specifications, and how to e.g. override arguments. + +In order to build (or "bake") a certain image, find the image's target (name) in the *docker-bake.hcl* file, and then run `docker buildx bake `. + +```bash +# build the dummy image that will crash on 5k invs +docker buildx bake bitcoin-5k-inv + +# build the same image, but set platform to only linux/amd64 +docker buildx bake bitcoin-5k-inv --set bitcoin-5k-inv.platform=linux/amd64 +``` + +To load the single-platform build result to `docker images`, run: + +```bash +docker buildx bake --load bitcoin-5k-inv +``` + +Push the build result to a registry by running: + +```bash +docker buildx bake --push bitcoin-5k-inv +``` -Once a new image has been pushed, it should be referenced in manifests/warnet-rpc-statefulset.yaml in the `image` field. +It will automatically push the build result to registry. diff --git a/docs/graph.md b/docs/graph.md deleted file mode 100644 index 3b3a05c46..000000000 --- a/docs/graph.md +++ /dev/null @@ -1,87 +0,0 @@ -# Warnet Network Topology - -Warnet creates a Bitcoin network using a network topology from a [graphml](https://graphml.graphdrawing.org/specification.html) file. - -Before any scenarios or RPC commands can be executed, a Warnet network must be started from a graph. -See [warcli.md](warcli.md) for more details on these commands. - -To start a network called `"warnet"` from the [default graph file](../graphs/default.graphml): -``` -warcli network start -``` - -To start a network with custom configurations: -``` -warcli network start --network="network_name" -``` - -## Creating graphs automatically - -Graphs can be created via the graph menu: - -```bash -# show graph commands -warcli graph --help - -# Create a cycle graph of 12 nodes using default Bitcoin Core version (v26.0) -warcli graph create 12 --outfile=./12_x_v26.0.graphml - -# Start network with default name "warnet" -warcli network start ./12_x_v26.0.graphml -``` - -## Warnet graph nodes and edges - -Nodes in a Warnet graph MUST have either a `"version"` key or an `"image"` key. -These dictate what version of Bitcoin Core to deploy in a fiven tank. - -Edges without additional properties are interpreted as Bitcoin p2p connections. -If an edge has additional key-value properties, it will be interpreted as a -lightning network channel (see [lightning.md](lightning.md)). - -## GraphML file specification - -### GraphML file format and headers -```xml - - - - - - - - - - - - - - - - - - - - - - -``` - -| key | for | type | default | explanation | -|----------------|-------|---------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| services | graph | string | | A space-separated list of extra service containers to deploy in the network. See [docs/services.md](services.md) for complete list of available services | -| version | node | string | | Bitcoin Core version with an available Warnet tank image on Dockerhub. May also be a GitHub repository with format user/repository:branch to build from source code | -| image | node | string | | Bitcoin Core Warnet tank image on Dockerhub with the format repository/image:tag | -| bitcoin_config | node | string | | A string of Bitcoin Core options in command-line format, e.g. '-debug=net -blocksonly' | -| tc_netem | node | string | | A tc-netem command as a string beginning with 'tc qdisc add dev eth0 root netem' | -| exporter | node | boolean | False | Whether to attach a Prometheus data exporter to the tank | -| metrics | node | string | | A space-separated string of RPC queries to scrape by Prometheus | -| collect_logs | node | boolean | False | Whether to collect Bitcoin Core debug logs with Promtail | -| build_args | node | string | | A string of configure options used when building Bitcoin Core from source code, e.g. '--without-gui --disable-tests' | -| ln | node | string | | Attach a lightning network node of this implementation (currently only supports 'lnd' or 'cln') | -| ln_image | node | string | | Specify a lightning network node image from Dockerhub with the format repository/image:tag | -| ln_cb_image | node | string | | Specify a lnd Circuit Breaker image from Dockerhub with the format repository/image:tag | -| ln_config | node | string | | A string of arguments for the lightning network node in command-line format, e.g. '--protocol.wumbo-channels --bitcoin.timelockdelta=80' | -| channel_open | edge | string | | Indicate that this edge is a lightning channel with these arguments passed to lnd openchannel | -| source_policy | edge | string | | Update the channel originator policy by passing these arguments passed to lnd updatechanpolicy | -| target_policy | edge | string | | Update the channel partner policy by passing these arguments passed to lnd updatechanpolicy | diff --git a/docs/install.md b/docs/install.md index e04d58267..cd2f08edd 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,88 +1,115 @@ -# Install Warnet +# Installing Warnet -Warnet requires Kubernetes in order to run the network. Kubernetes can be run -remotely or locally (with minikube or Docker Desktop). `kubectl` must be run -locally to administer the network. +Warnet runs on Kubernetes (k8s) and requires the Helm Kubernetes package manager in order to run the network. +The Kubernetes cluster can be run locally via minikube, Docker Desktop, k3d or similar, or remotely via Googles GKE, Digital Ocean, etc.. +The utilities `kubectl` and `helm` must be installed and found on $PATH to administer the network. -## Dependencies - -### Kubernetes - -Install [`kubectl`](https://kubernetes.io/docs/setup/) (or equivalent) and -configure your cluster. This can be done locally with `minikube` (or Docker Desktop) -or using a managed cluster. +## Install Warnet -#### Docker engine with minikube +Either install warnet via pip, or clone the source and install: -If using Minikube to run a smaller-sized local cluster, you will require docker engine. -To install docker engine, see: https://docs.docker.com/engine/install/ +### via pip -e.g. For Ubuntu: +You can install warnet via `pip` into a virtual environment with ```bash -# First uninstall any old versions -for pkg in docker.io docker-doc podman-docker containerd runc; do sudo apt-get remove $pkg; done - -# Add Docker's official GPG key: -sudo apt-get update -sudo apt-get install ca-certificates curl gnupg -sudo install -m 0755 -d /etc/apt/keyrings -curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg -sudo chmod a+r /etc/apt/keyrings/docker.gpg - -# Add the repository to Apt sources: -echo \ - "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ - "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ - sudo tee /etc/apt/sources.list.d/docker.list > /dev/null -sudo apt-get update - -# Install the docker packages -sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin +python3 -m venv .venv +source .venv/bin/activate +pip install warnet ``` -#### Using Docker +### via cloned source + +You can install warnet from source into a virtual environment with -If you have never used Docker before you may need to take a few more steps to run the Docker daemon on your system. -The Docker daemon MUST be running before stating Warnet. +```bash +git clone https://github.com/bitcoin-dev-project/warnet.git +cd warnet +python3 -m venv .venv +source .venv/bin/activate +pip install -e . +``` -##### Linux +## Dependencies -- [Check Docker user/group permissions](https://stackoverflow.com/a/48957722/1653320) -- or [`chmod` the Docker UNIX socket](https://stackoverflow.com/a/51362528/1653320) +The [`helm`](https://helm.sh/) and [`kubectl`](https://kubernetes.io/docs/reference/kubectl/) utilities are required for all configurations to talk to and administrate your cluster. +These can be installed using your operating system's package manager, a third party package manager like [homebrew](https://brew.sh/), or as binaries directly into a python virtual environment created for warnet, by following the steps in [Use warnet to install dependencies](#use-warnet-to-install-dependencies). -## Install Warnet +If you are using a cloud-based cluster, these are the only tools needed. -### Recommended: use a virtual Python environment such as `venv` +### Use warnet to install dependencies ```bash -python3 -m venv .venv # Use alternative venv manager if desired +# Ensure the virtual environment is active source .venv/bin/activate + +# Run `warnet setup` to be guided through downloading binaries into the +# python virtual environment +warnet setup ``` -```bash -pip install --upgrade pip -pip install warnet +### Running Warnet Locally + +If the number of nodes you are running can run on one machine (think a dozen or so) then Warnet can happily run on a local Kubernetes. +Two supported local Kubernetes implementations are Minikube and Docker Desktop. + +#### Docker Desktop + +[Docker desktop](https://www.docker.com/products/docker-desktop/) includes the docker engine itself and has an option to enable Kubernetes. +Install it and enable Kubernetes in the option menu to start a cluster. + +#### Minikube + +Minikube requires a backend to run on with the supported backend being Docker. + +[Install Docker](https://docs.docker.com/engine/install/) first, and then proceed to [Install Minkube](https://minikube.sigs.k8s.io/docs/start/). + +After installing Minikube don't forget to start it with: + +```shell +minikube start ``` -## Contributing / Local Warnet Development +Minikube has a [guide](https://kubernetes.io/docs/tutorials/hello-minikube/) on getting started which could be useful to validate that your minikube is running correctly. -### Download the code repository +## Testing kubectl and helm -```bash -git clone https://github.com/bitcoin-dev-project/warnet -cd warnet +After installing `kubectl` and `helm` the following commands should run successfully on either a local or remote cluster. +Do not proceed unless `kubectl` and `helm` are working. + +```shell +helm repo add examples https://helm.github.io/examples +helm install hello examples/hello-world +helm list +kubectl get pods +helm uninstall hello ``` -### Recommended: use a virtual Python environment such as `venv` +#### Managing a Kubernetes cluster + +The use of a k8s cluster management tool is highly recommended. +We like to use `k9s`: https://k9scli.io/ + +## Running + +To get started first check you have all the necessary requirements: + +> [!TIP] +> Don't forget to activate your python virtual environment when using new terminals! ```bash -python3 -m venv .venv # Use alternative venv manager if desired -source .venv/bin/activate +warnet setup ``` +Then create your first network: + ```bash -pip install --upgrade pip -pip install -e . +# Create a new network in the current directory +warnet init + +# Or in a directory of choice +warnet new ``` +> [!TIP] +> If you are having stability issues it could be due to resource constraints. Check out these tips for [scaling](scaling.md). \ No newline at end of file diff --git a/docs/lightning.md b/docs/lightning.md deleted file mode 100644 index 2ae634851..000000000 --- a/docs/lightning.md +++ /dev/null @@ -1,97 +0,0 @@ -# Lightning Network - -## Adding LN nodes to graph - -LN nodes can be added to any Bitcoin Core node by adding a data element with key -`"ln"` to the node in the graph file. The value is the LN implementation desired. - -**Currently only `lnd` is supported** - -Example: - -``` - - lnd - -``` - -## Adding LN channels to graph - -LN channels are represented in the graphml file as edges with extra data elements -that correspond to arguments to the lnd `openchannel` and `updatechanpolicy` RPC -commands. The keys are: - -- `"channel_open"` (arguments added to `openchannel`) -- `"target_policy"` or `"source_policy"` (arguments added to `updatechanpolicy`) - -The key `"channel_open"` is required to open a LN channel in warnet, and to -identify an edge in the graphml file as a LN channel. - -Example: - -``` - - --local_amt=100000 - --base_fee_msat=100 --fee_rate_ppm=5 --time_lock_delta=18 - --base_fee_msat=2200 --fee_rate_ppm=13 --time_lock_delta=20 - -``` - -A complete example graph with LN nodes and channels is included in the test -data directory: [ln.graphml](../test/data/ln.graphml) - -## Running the Lightning network - -When warnet is started with `warcli network start` the bitcoin containers will -be started first followed by the lightning node containers. It may require a few -automatic restarts before the lightning nodes start up and connect to their -corresponding bitcoin nodes. Use `warcli network status` to monitor container status -and wait for all containers to be `running`. - -To create the lightning channels specified in the graph file, run the included -scenario: - -`warcli scenarios run ln_init` - -This [scenario](../src/scenarios/ln_init.py) will generate blocks, fund the wallets -in the bitcoin nodes, and open the channels from the graph. Each of these steps -requires some waiting as transactions are confirmed in the warnet blockchain -and lightning nodes gossip their channel announcements to each other. -Use `warcli scenarios active` to monitor the status of the scenario. When it is -complete the subprocess will exit and it will indicate `Active: False`. At that -point, the lightning network is ready for activity. - -## sim-ln compatibility - -Warnet can export data required to run [sim-ln](https://github.com/bitcoin-dev-project/sim-ln) -with a warnet network. - -With a network running, execute: `warcli network export` with optional argument -`--network=` (default is "warnet"). This will copy all lightning -node credentials like SSL certificates and macaroons into a local directory as -well as generate a JSON file required by sim-ln. - -Example (see sim-ln docs for exact API): - -``` -$ warcli network export -/Users/bitcoin-dev-project/.warnet/warnet/warnet/simln - -$ ls /Users/bitcoin-dev-project/.warnet/warnet/warnet/simln -sim.json warnet_ln_000000_tls.cert warnet_ln_000001_tls.cert warnet_ln_000002_tls.cert -warnet_ln_000000_admin.macaroon warnet_ln_000001_admin.macaroon warnet_ln_000002_admin.macaroon - -$ sim-cli --data-dir /Users/bitcoin-dev-project/.warnet/warnet/warnet/simln -2023-11-18T16:58:28.731Z INFO [sim_cli] Connected to warnet_ln_000000 - Node ID: 031b1404744431b01ee4fa2bfc3c5caa1f1044ff5a9cb553d2c8ec6eb0f9d8040c. -2023-11-18T16:58:28.747Z INFO [sim_cli] Connected to warnet_ln_000001 - Node ID: 02318b75bd91bf6265b30fe97f8ebbb0eda85194cf9d4467d43374de0248c7bf05. -2023-11-18T16:58:28.760Z INFO [sim_cli] Connected to warnet_ln_000002 - Node ID: 0393aa24d777e2391b5238c485ecce08b35bd9aa4ddf4f2226016107c6829804d5. -2023-11-18T16:58:28.760Z INFO [sim_lib] Running the simulation forever. -2023-11-18T16:58:28.815Z INFO [sim_lib] Simulation is running on regtest. -2023-11-18T16:58:28.815Z INFO [sim_lib] Simulating 0 activity on 3 nodes. -2023-11-18T16:58:28.815Z INFO [sim_lib] Summary of results will be reported every 60s. -2023-11-18T16:58:28.826Z INFO [sim_lib] Generating random activity with multiplier: 2, average payment amount: 3800000. -2023-11-18T16:58:28.826Z INFO [sim_lib] Created network generator: network graph view with: 3 channels. -2023-11-18T16:58:28.826Z INFO [sim_lib] Started random activity producer for warnet_ln_000000(031b14...d8040c): activity generator for capacity: 50000000 with multiplier 2: 26.31578947368421 payments per month (0.03654970760233918 per hour). -2023-11-18T16:58:28.826Z INFO [sim_lib] Started random activity producer for warnet_ln_000001(02318b...c7bf05): activity generator for capacity: 100000000 with multiplier 2: 52.63157894736842 payments per month (0.07309941520467836 per hour). -2023-11-18T16:58:28.826Z INFO [sim_lib] Started random activity producer for warnet_ln_000002(0393aa...9804d5): activity generator for capacity: 50000000 with multiplier 2: 26.31578947368421 payments per month (0.03654970760233918 per hour). -``` diff --git a/docs/logging_monitoring.md b/docs/logging_monitoring.md index c205806aa..07c0be2c8 100644 --- a/docs/logging_monitoring.md +++ b/docs/logging_monitoring.md @@ -1,31 +1,23 @@ # Logging and Monitoring -Warnet allows different granularity of logging. - ## Logging -### Warnet network level logging - -Fetch logs from the warnet RPC server `rpc-0`, which is in charge of orchestrating the network. - -Examples of information provided: +### Pod logs -- how many tanks are running -- what scenarios are running -- warnet RPC requests +The command `warnet logs` will bring up a menu of pods to print log output from, +such as Bitcoin tanks, or scenario commanders. Follow the output with the `-f` option. -Commands: `warcli network logs` or `warcli network logs --follow`. - -See more details in [warcli](/docs/warcli.md#warcli-network-logs) +See command [`warnet logs`](/docs/warnet.md#warnet-logs) ### Bitcoin Core logs -These are tank level or pod level log output from a Bitcoin Core node, useful for things like net logging and transaction propagation, retrieved by RPC `debug-log` using its network name and graph node index. +Entire debug log files from a Bitcoin tank can be dumped by using the tank's +pod name. Example: ```sh -$ warcli bitcoin debug-log 0 +$ warnet bitcoin debug-log tank-0000 2023-10-11T17:54:39.616974Z Bitcoin Core version v25.0.0 (release build) @@ -34,50 +26,47 @@ $ warcli bitcoin debug-log 0 ... (etc) ``` -For logs of lightning nodes, kubectl is required. +See command [`warnet bitcoin debug-log`](/docs/warnet.md#warnet-bitcoin-debug-log) + +### Aggregated logs from all Bitcoin nodes -### Aggregated logs from all nodes +Aggregated logs can be searched using `warnet bitcoin grep-logs` with regex patterns. -Aggregated logs can be searched using `warcli bitcoin grep-logs` with regex patterns. +See more details in [`warnet bitcoin grep-logs`](/docs/warnet.md#warnet-bitcoin-grep-logs) Example: ```sh -$ warcli bitcoin grep-logs 94cacabc09b024b56dcbed9ccad15c90340c596e883159bcb5f1d2152997322d - -warnet_test_uhynisdj_tank_000001: 2023-10-11T17:44:48.716582Z [miner] AddToWallet 94cacabc09b024b56dcbed9ccad15c90340c596e883159bcb5f1d2152997322d newupdate -warnet_test_uhynisdj_tank_000001: 2023-10-11T17:44:48.717787Z [miner] Submitting wtx 94cacabc09b024b56dcbed9ccad15c90340c596e883159bcb5f1d2152997322d to mempool for relay -warnet_test_uhynisdj_tank_000001: 2023-10-11T17:44:48.717929Z [validation] Enqueuing TransactionAddedToMempool: txid=94cacabc09b024b56dcbed9ccad15c90340c596e883159bcb5f1d2152997322d wtxid=0cc875e73bb0bd8f892b70b8d1e5154aab64daace8d571efac94c62b8c1da3cf -warnet_test_uhynisdj_tank_000001: 2023-10-11T17:44:48.718040Z [validation] TransactionAddedToMempool: txid=94cacabc09b024b56dcbed9ccad15c90340c596e883159bcb5f1d2152997322d wtxid=0cc875e73bb0bd8f892b70b8d1e5154aab64daace8d571efac94c62b8c1da3cf -warnet_test_uhynisdj_tank_000001: 2023-10-11T17:44:48.723017Z [miner] AddToWallet 94cacabc09b024b56dcbed9ccad15c90340c596e883159bcb5f1d2152997322d -warnet_test_uhynisdj_tank_000007: 2023-10-11T17:44:52.173199Z [validation] Enqueuing TransactionAddedToMempool: txid=94cacabc09b024b56dcbed9ccad15c90340c596e883159bcb5f1d2152997322d wtxid=0cc875e73bb0bd8f892b70b8d1e5154aab64daace8d571efac94c62b8c1da3cf +$ warnet bitcoin grep-logs 94cacabc09b024b56dcbed9ccad15c90340c596e883159bcb5f1d2152997322d + +tank-0001: 2023-10-11T17:44:48.716582Z [miner] AddToWallet 94cacabc09b024b56dcbed9ccad15c90340c596e883159bcb5f1d2152997322d newupdate +tank-0001: 2023-10-11T17:44:48.717787Z [miner] Submitting wtx 94cacabc09b024b56dcbed9ccad15c90340c596e883159bcb5f1d2152997322d to mempool for relay +tank-0001: 2023-10-11T17:44:48.717929Z [validation] Enqueuing TransactionAddedToMempool: txid=94cacabc09b024b56dcbed9ccad15c90340c596e883159bcb5f1d2152997322d wtxid=0cc875e73bb0bd8f892b70b8d1e5154aab64daace8d571efac94c62b8c1da3cf +tank-0001: 2023-10-11T17:44:48.718040Z [validation] TransactionAddedToMempool: txid=94cacabc09b024b56dcbed9ccad15c90340c596e883159bcb5f1d2152997322d wtxid=0cc875e73bb0bd8f892b70b8d1e5154aab64daace8d571efac94c62b8c1da3cf +tank-0001: 2023-10-11T17:44:48.723017Z [miner] AddToWallet 94cacabc09b024b56dcbed9ccad15c90340c596e883159bcb5f1d2152997322d +tank-0002: 2023-10-11T17:44:52.173199Z [validation] Enqueuing TransactionAddedToMempool: txid=94cacabc09b024b56dcbed9ccad15c90340c596e883159bcb5f1d2152997322d wtxid=0cc875e73bb0bd8f892b70b8d1e5154aab64daace8d571efac94c62b8c1da3cf ... (etc) ``` -See more details in [warcli](/docs/warcli.md#warcli-bitcoin-grep-logs) ## Monitoring and Metrics ## Install logging infrastructure -Ensure that [`helm`](https://helm.sh/docs/intro/install/) is installed, then simply run the following scripts: +If any tank in a network is configured with `collectLogs: true` or `metricsExport: true` +then the logging stack will be installed automatically when `warnet deploy` is executed. -```bash -./resources/scripts/install_logging.sh -``` +The logging stack includes Loki, Prometheus, and Grafana. Together these programs +aggregate logs and data from Bitcoin RPC queries into a web-based dashboard. -To forward port `3000` and view the [Grafana](#grafana) dashboard run the `connect_logging` script: +## Connect to logging dashboard -```bash -./resources/scripts/connect_logging.sh -``` - -It might take a couple minutes to get the pod running. If you see `error: unable to forward port because pod is not running. Current status=Pending`, hang tight. +The logging stack including the user interface web server runs inside the kubernetes cluster. +Warnet will forward port `2019` locally from the cluster, and the landing page for all +web based interfaces will be available at `localhost:2019`. -The Grafana dashboard (and API) will be accessible without requiring authentication -at `http://localhost:3000`. +This page can also be opened quickly with the command [`warnet dashboard`](/docs/warnet.md#warnet-dashboard) -The `install_logging` script will need to be installed before starting the network in order to collect the information for monitoring and metrics. Restart the network with `warcli network down && warcli network up` if necessary. ### Prometheus @@ -86,14 +75,7 @@ to any Bitcoin Tank and configured to scrape any available RPC results. The `bitcoin-exporter` image is defined in `resources/images/exporter` and maintained in the BitcoinDevProject dockerhub organization. To add the exporter -in the Tank pod with Bitcoin Core add the `"exporter"` key to the node in the graphml file: - -```xml - - 27.0 - true - -``` +in the Tank pod with Bitcoin Core add the `metricsExport: true` value to the node in the yaml file. The default metrics are defined in the `bitcoin-exporter` image: - Block count @@ -101,25 +83,23 @@ The default metrics are defined in the `bitcoin-exporter` image: - Number of outbound peers - Mempool size (# of TXs) -Metrics can be configured by setting a `"metrics"` key to the node in the graphml file. -The metrics value is a space-separated list of labels, RPC commands with arguments, and +Metrics can be configured by setting an additional `metrics` value to the node in the yaml file. The metrics value is a space-separated list of labels, RPC commands with arguments, and JSON keys to resolve the desired data: ``` label=method(arguments)[JSON result key][...] ``` -For example, the default metrics listed above are defined as: +For example, the default metrics listed above would be explicitly configured as follows: -```xml - - 27.0 - true - blocks=getblockcount() inbounds=getnetworkinfo()["connections_in"] outbounds=getnetworkinfo()["connections_in"] mempool_size=getmempoolinfo()["size"] - +```yaml +nodes: + - name: tank-0000 + metricsExport: true + metrics: blocks=getblockcount() inbounds=getnetworkinfo()["connections_in"] outbounds=getnetworkinfo()["connections_out"] mempool_size=getmempoolinfo()["size"] ``` -The data can be retrieved from the Prometheus exporter on port `9332`, example: +The data can be retrieved directly from the Prometheus exporter container in the tank pod via port `9332`, example: ``` # HELP blocks getblockcount() @@ -128,7 +108,7 @@ blocks 704.0 # HELP inbounds getnetworkinfo()["connections_in"] # TYPE inbounds gauge inbounds 0.0 -# HELP outbounds getnetworkinfo()["connections_in"] +# HELP outbounds getnetworkinfo()["connections_out"] # TYPE outbounds gauge outbounds 0.0 # HELP mempool_size getmempoolinfo()["size"] @@ -136,28 +116,24 @@ outbounds 0.0 mempool_size 0.0 ``` -### Grafana +### Defining lnd metrics to capture -Data from Prometheus exporters can be collected and fed into Grafana for a -web-based interface. - -#### Dashboards +Lightning nodes can also be configured to export metrics to prometheus using `lnd-exporter`. +Example configuration is provided in `test/data/ln/`. Review `node-defauts.yaml` for a typical logging configuration. All default metrics reported to prometheus are prefixed with `lnd_` -To view the default metrics in the included default dashboard, upload the dashboard -JSON file to the Grafana server: +[lnd-exporter configuration reference](https://github.com/bitcoin-dev-project/lnd-exporter/tree/main?tab=readme-ov-file#configuration) +lnd-exporter assumes same macaroon referenced in ln_framework (can be overridden by env variable) -```sh -curl localhost:3000/api/dashboards/db \ - -H "Content-Type: application/json" \ - --data "{\"dashboard\": $(cat resources/configs/grafana/default_dashboard.json)}" -``` +**Note: `test/data/ln` and `test/data/logging` take advantage of **extraContainers** configuration option to add containers to default `lnd/templates/pod`* -Note the URL in the reply from the server (example): +### Grafana -```sh -{"folderUid":"","id":2,"slug":"default-warnet-dashboard","status":"success","uid":"fdu0pda1z6a68b","url":"/https/github.com/d/fdu0pda1z6a68b/default-warnet-dashboard","version":1}( -``` +Data from Prometheus exporters is collected and fed into Grafana for a +web-based interface. -Open the dashboard in your browser (example): +#### Dashboards -`http://localhost:3000/d/fdu0pda1z6a68b/default-warnet-dashboard` +Grafana dashboards are described in JSON files. A default Warnet dashboard +is included and any other json files in the `/resources/charts/grafana-dashboards/files` directory +will also be deployed to the web server. The Grafana UI itself also has an API +for creating, exporting, and importing other dashboard files. diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 000000000..bce833864 --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,72 @@ +# Plugins + +Plugins extend Warnet. Plugin authors can import commands from Warnet and interact with the kubernetes cluster, and plugin users can run plugins from the command line or from the `network.yaml` file. + +## Activating plugins from `network.yaml` + +You can activate a plugin command by placing it in the `plugins` section at the bottom of each `network.yaml` file like so: + +````yaml +nodes: + <> + +plugins: # This marks the beginning of the plugin section + preDeploy: # This is a hook. This particular hook will call plugins before deploying anything else. + hello: # This is the name of the plugin. + entrypoint: "../plugins/hello" # Every plugin must specify a path to its entrypoint. + podName: "hello-pre-deploy" # Plugins can have their own particular configurations, such as how to name a pod. + helloTo: "preDeploy!" # This configuration tells the hello plugin who to say "hello" to. +```` + +## Many kinds of hooks +There are many hooks to the Warnet `deploy` command. The example below specifies them: + +````yaml +nodes: + <> + +plugins: + preDeploy: # Plugins will run before any other `deploy` code. + hello: + entrypoint: "../plugins/hello" + podName: "hello-pre-deploy" + helloTo: "preDeploy!" + postDeploy: # Plugins will run after all the `deploy` code has run. + simln: + entrypoint: "../plugins/simln" + activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' + hello: + entrypoint: "../plugins/hello" + podName: "hello-post-deploy" + helloTo: "postDeploy!" + preNode: # Plugins will run before `deploy` launches a node (once per node). + hello: + entrypoint: "../plugins/hello" + helloTo: "preNode!" + postNode: # Plugins will run after `deploy` launches a node (once per node). + hello: + entrypoint: "../plugins/hello" + helloTo: "postNode!" + preNetwork: # Plugins will run before `deploy` launches the network (essentially between logging and when nodes are deployed) + hello: + entrypoint: "../plugins/hello" + helloTo: "preNetwork!" + podName: "hello-pre-network" + postNetwork: # Plugins will run after the network deploy threads have been joined. + hello: + entrypoint: "../plugins/hello" + helloTo: "postNetwork!" + podName: "hello-post-network" +```` + +Warnet will execute these plugin commands during each invocation of `warnet deploy`. + + + +## A "hello" example + +To get started with an example plugin, review the `README` of the `hello` plugin found in any initialized Warnet directory: + +1. `warnet init` +2. `cd plugins/hello/` + diff --git a/docs/quickrun.md b/docs/quickrun.md deleted file mode 100644 index 321e7f92b..000000000 --- a/docs/quickrun.md +++ /dev/null @@ -1,66 +0,0 @@ -# Quick run - -## Installation - -Either install warnet via pip, or clone the source and install: - -### via pip - -You can install warnet via `pip` into a virtual environment with - -```bash -python3 -m venv .venv -source .venv/bin/activate -pip install warnet -``` - -### via cloned source - -You can install warnet from source into a virtual environment with - -```bash -git clone https://github.com/bitcoin-dev-project/warnet.git -cd warnet -python3 -m venv .venv -source .venv/bin/activate -pip install -e . -``` - -## Running - -> [!TIP] -> When developing locally add the `--dev` flag to `warcli cluster deploy` to enable dev mode with hot-reloading server. - -### Using minikube - -To run a local cluster using minikube: - -```bash -warcli cluster setup-minikube - -warcli cluster deploy -``` - -### Other cluster types - -If not using minikube (e.g. using Docker Desktop or a managed cluster), `warcli` commands will operate natively on the current Kubernetes context, so you can simply run: - -```bash -warcli cluster deploy -``` - -...to deploy warnet to your cluster. - -`warcli cluster deploy` also automatically configures port forwarding to the Server in the cluster. - -## Stopping - -To tear down the cluster: - -```bash -warcli cluster teardown -``` - -## Log location - -If the `$XDG_STATE_HOME` environment variable is set, the server will log to a file `$XDG_STATE_HOME/warnet/warnet.log`, otherwise it will use `$HOME/.warnet/warnet.log`. diff --git a/docs/random_internet_as_graph_n100.png b/docs/random_internet_as_graph_n100.png deleted file mode 100644 index 3e0fd3422..000000000 Binary files a/docs/random_internet_as_graph_n100.png and /dev/null differ diff --git a/docs/release-process.md b/docs/release-process.md deleted file mode 100644 index 6f4c28090..000000000 --- a/docs/release-process.md +++ /dev/null @@ -1,29 +0,0 @@ -# Release process - -Once a tag is pushed to GH this will start an image build using the tag - -## Prerequisites - -- [ ] Update version in pyproject.toml -- [ ] Tag git with new version -- [ ] Push tag to GitHub - -## Manual Builds - -```bash -# Install build dependencies -pip install -e .[build] - -# Remove previous release metadata -rm -i -Rf build/ dist/ - -# Build wheel -python3 -m build -``` - -### Upload - -```bash -# Upload to Pypi -python3 -m twine upload dist/* -``` diff --git a/docs/running.md b/docs/running.md deleted file mode 100644 index 04339a329..000000000 --- a/docs/running.md +++ /dev/null @@ -1,38 +0,0 @@ -# Running Warnet - -Warnet runs a server which can be used to manage multiple networks. On Kubernetes -this runs as a `statefulSet` in the cluster. - -See more details in [warcli](/docs/warcli.md), examples: - -To start the server run: - -```bash -warcli cluster deploy -``` - -Start a network from a graph file: - -```bash -warcli network start resources/graphs/default.graphml -``` - -Make sure all tanks are running with: - -```bash -warcli network status -``` - -Check if the edges of the graph (bitcoin p2p connections) are complete: - -```bash -warcli network connected -``` - -_Optional_ Check out the logs with: - -```bash -warcli network logs -f -``` - -If that looks all good, give [scenarios](/docs/scenarios.md) a try. diff --git a/docs/scaling.md b/docs/scaling.md index 066827122..39b74208a 100644 --- a/docs/scaling.md +++ b/docs/scaling.md @@ -1,5 +1,8 @@ # Running large networks +> [!NOTE] +> These changes are not required on multi-host/managed clusters + When running a large number of containers on a single host machine, the system may run out of various resources. We recommend setting the following values in /etc/sysctl.conf: diff --git a/docs/scenarios.md b/docs/scenarios.md index acdc470b2..c2892c421 100644 --- a/docs/scenarios.md +++ b/docs/scenarios.md @@ -2,53 +2,73 @@ Scenarios are written using the Bitcoin Core test framework for functional testing, with some modifications: most notably that `self.nodes[]` represents an array of -containerized `bitcoind` nodes ("tanks"). Scenario files are run with a python interpreter -inside the server and can control many nodes in the network simultaneously. +containerized `bitcoind` nodes ("tanks"). -See [`src/warnet/scenarios`](../src/warnet/scenarios) for examples of how these can be written. +Scenario files are run with a python interpreter inside their own pod called a "commander" +in kubernetes and many can be run simultaneously. The Commander is provided with +a JSON file describing the Bitcoin nodes it has access to via RPC. -To see available scenarios (loaded from the default directory): +See [`resources/scenarios/`](../resources/scenarios/) for examples of how these can be written. +When creating a new network default scenarios will be copied into your project directory for convenience. -```bash -warcli scenarios available -``` - -Once a scenario is selected it can be run with `warcli scenarios run [--network=warnet] [scenario_params]`. - -The [`miner_std`](../src/warnet/scenarios/miner_std.py) scenario is a good one to start with as it automates block generation: - -```bash -# Have all nodes generate a block 5 seconds apart in a round-robin -warcli scenarios run miner_std --allnodes --interval=5 -``` +A scenario can be run with `warnet run [optional_params]`. -This will run the scenario in a background thread on the server until it exits or is stopped by the user. - -Active scenarios can be listed and terminated by PID: +The [`miner_std`](../resources/scenarios/miner_std.py) scenario is a good one to start with as it automates block generation: ```bash -$ warcli scenarios available -miner_std Generate blocks over time. Options: [--allnodes | --interval= | --mature] -sens_relay Send a transaction using sensitive relay -tx_flood Generate 100 blocks with 100 TXs each - -$ warcli scenarios run tx_flood -Running scenario tx_flood with PID 14683 in the background... - -$ warcli scenarios active - ┃ Active ┃ Cmd ┃ Network ┃ Pid ┃ Return_code ┃ - ┡━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━┩ - │ True │ tx_flood │ warnet │ 14683 │ None ┃ - -$ warcli scenarios stop 14683 -Stopped scenario with PID 14683. +₿ warnet run build55/scenarios/miner_std.py --allnodes --interval=10 +configmap/warnetjson configured +configmap/scenariopy configured +pod/commander-minerstd-1724708498 created +Successfully started scenario: miner_std +Commander pod name: commander-minerstd-1724708498 + +₿ warnet status +╭──────────────────── Warnet Overview ────────────────────╮ +│ │ +│ Warnet Status │ +│ ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓ │ +│ ┃ Component ┃ Name ┃ Status ┃ │ +│ ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩ │ +│ │ Tank │ tank-0001 │ running │ │ +│ │ Tank │ tank-0002 │ running │ │ +│ │ Tank │ tank-0003 │ running │ │ +│ │ Tank │ tank-0004 │ running │ │ +│ │ Tank │ tank-0005 │ running │ │ +│ │ Tank │ tank-0006 │ running │ │ +│ │ │ │ │ │ +│ │ Scenario │ commander-minerstd-1724708498 │ pending │ │ +│ └───────────┴───────────────────────────────┴─────────┘ │ +│ │ +╰─────────────────────────────────────────────────────────╯ + +Total Tanks: 6 | Active Scenarios: 1 + +₿ warnet stop commander-minerstd-1724708498 +pod "commander-minerstd-1724708498" deleted +Successfully stopped scenario: commander-minerstd-1724708498 + +₿ warnet status +╭─────────────── Warnet Overview ───────────────╮ +│ │ +│ Warnet Status │ +│ ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┓ │ +│ ┃ Component ┃ Name ┃ Status ┃ │ +│ ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━┩ │ +│ │ Tank │ tank-0001 │ running │ │ +│ │ Tank │ tank-0002 │ running │ │ +│ │ Tank │ tank-0003 │ running │ │ +│ │ Tank │ tank-0004 │ running │ │ +│ │ Tank │ tank-0005 │ running │ │ +│ │ Tank │ tank-0006 │ running │ │ +│ │ Scenario │ No active scenarios │ │ │ +│ └───────────┴─────────────────────┴─────────┘ │ +│ │ +╰───────────────────────────────────────────────╯ + +Total Tanks: 6 | Active Scenarios: 0 ``` ## Running a custom scenario -You can write your own scenario file locally and upload it to the server with -the [run-file](/docs/warcli.md#warcli-scenarios-run-file) command (example): - -```bash -warcli scenarios run-file /home/me/bitcoin_attack.py -``` +You can write your own scenario file and run it in the same way. diff --git a/docs/snapshots.md b/docs/snapshots.md new file mode 100644 index 000000000..ab0001fbd --- /dev/null +++ b/docs/snapshots.md @@ -0,0 +1,94 @@ +# Creating and loading warnet snapshots + +The `snapshot` command allows users to create snapshots of Bitcoin data directories from active bitcoin nodes. These snapshots can be used for backup purposes, to recreate specific network states, or to quickly initialize new bitcoin nodes with existing data. + +## Usage Examples + +### Snapshot a Specific bitcoin node + +To create a snapshot of a specific bitcoin node: + +```bash +warnet snapshot my-node-name -o +``` + +This will create a snapshot of the bitcoin node named "my-node-name" in the ``. If a directory the directory does not exist, it will be created. If no directory is specified, snapshots will be placed in `./warnet-snapshots` by default. + +### Snapshot all nodes + +To snapshot all running bitcoin nodes: + +```bash +warnet snapshot --all -o `` +``` + +### Use Filters + +In the previous examples, everything in the bitcoin datadir was included in the snapshot, e.g., peers.dat. But there maybe use cases where only certain directories are needed. For example, assuming you only want to save the chain up to that point, you can use the filter argument: + +```bash +warnet snapshot my-node-name --filter "chainstate,blocks" +``` + +This will create a snapshot containing only the 'blocks' and 'chainstate' directories. You would only need to snapshot this for a single node since the rest of the nodes will get this data via IBD when this snapshot is later loaded. A few other useful filters are detailed below: + +```bash +# snapshot the logs from all nodes +warnet snapshot --all -f debug.log -o ./node-logs + +# snapshot the chainstate and wallet from a mining node +# this is particularly userful for premining a signet chain that +# can be used later for starting a signet network +warnet snapshot mining-node -f "chainstate,blocks,wallets" + +# snapshot only the wallets from a node +warnet snapshot my-node -f wallets + +# snapshot a specific wallet +warnet snapshot my-node -f mining_wallet +``` + +## End-to-End Example + +Here's a step-by-step guide on how to create a snapshot, upload it, and configure Warnet to use this snapshot when deploying. This particular example is for creating a premined signet chain: + +1. Create a snapshot of the mining node: + ```bash + warnet snapshot miner --output /tmp/snapshots --filter "blocks,chainstate,wallets" + ``` + +2. The snapshot will be created as a tar.gz file in the specified output directory. The filename will be in the format `{node_name}_{chain}_bitcoin_data.tar.gz`, i.e., `miner_bitcoin_data.tar.gz`. + +3. Upload the snapshot to a location accessible by your Kubernetes cluster. This could be a cloud storage service like AWS S3, Google Cloud Storage, or a GitHub repository. If working in a warnet project directory, you can commit your snapshot in a `snapshots/` folder. + +4. Note the URL of the uploaded snapshot, e.g., `https://github.com/your-username/your-repo/raw/main/my-warnet-project/snapshots/miner_bitcoin_data.tar.gz` + +5. Update your Warnet configuration to use this snapshot. This involves modifying your `network.yaml` configuration file. Here's an example of how to configure the mining node to use the snapshot: + + ```yaml + nodes: + - name: miner + image: + tag: "27.0" + loadSnapshot: + enabled: true + url: "https://github.com/your-username/your-repo/raw/main/snapshots/miner_bitcoin_data.tar.gz" + # ... other nodes ... + ``` + +6. Deploy Warnet with the updated configuration: + ```bash + warnet deploy networks/your_cool_network/network.yaml + ``` + +7. Warnet will now use the uploaded snapshot to initialize the Bitcoin data directory when creating the "miner" node. In this particular example, the blocks will then be distibuted to the other nodes via IBD and the mining node can resume signet mining off the chaintip by loading the wallet from the snapshot: + ```bash + warnet bitcoin rpc miner loadwallet mining_wallet + ``` + +## Notes + +- Snapshots are specific to the chain (signet, regtest) of the bitcoin node they were created from. Ensure you're using snapshots with the correct network when deploying. +- Large snapshots may take considerable time to upload and download. Consider using filters to reduce snapshot size if you don't need the entire data directory. +- Ensure that your Kubernetes cluster has the necessary permissions to access the location where you've uploaded the snapshot. +- When using GitHub to host snapshots, make sure to use the "raw" URL of the file for direct download. diff --git a/docs/warcli.md b/docs/warcli.md deleted file mode 100644 index 77dddae85..000000000 --- a/docs/warcli.md +++ /dev/null @@ -1,301 +0,0 @@ -# `warcli` - -The command-line interface tool for Warnet. - -Once `warnet` is running it can be interacted with using the cli tool `warcli`. - -Most `warcli` commands accept a `--network` option, which allows you to specify -the network you want to control. This is set by default to `--network="warnet"` -to simplify default operation. - -Execute `warcli --help` or `warcli help` to see a list of command categories. - -Help text is provided, with optional parameters in [square brackets] and required -parameters in . - -`warcli` commands are organized in a hierarchy of categories and subcommands. - -## API Commands - -### `warcli help` -Display help information for the given [command] (and sub-command). - If no command is given, display help for the main CLI. - -options: -| name | type | required | default | -|----------|--------|------------|-----------| -| commands | String | | | - -### `warcli setup` -Check Warnet requirements are installed - - -## Bitcoin - -### `warcli bitcoin debug-log` -Fetch the Bitcoin Core debug log from \ in [network] - -options: -| name | type | required | default | -|---------|--------|------------|-----------| -| node | Int | yes | | -| network | String | | "warnet" | - -### `warcli bitcoin grep-logs` -Grep combined logs via fluentd using regex \ - -options: -| name | type | required | default | -|---------------------|--------|------------|-----------| -| pattern | String | yes | | -| show_k8s_timestamps | Bool | | False | -| no_sort | Bool | | False | -| network | String | | "warnet" | - -### `warcli bitcoin messages` -Fetch messages sent between \ and \ in [network] - -options: -| name | type | required | default | -|---------|--------|------------|-----------| -| node_a | Int | yes | | -| node_b | Int | yes | | -| network | String | | "warnet" | - -### `warcli bitcoin rpc` -Call bitcoin-cli \ [params] on \ in [network] - -options: -| name | type | required | default | -|---------|--------|------------|-----------| -| node | Int | yes | | -| method | String | yes | | -| params | String | | | -| network | String | | "warnet" | - -## Cluster - -### `warcli cluster connect-logging` -Connect kubectl to cluster logging - - -### `warcli cluster deploy` -Deploy Warnet using the current kubectl-configured cluster - -options: -| name | type | required | default | -|--------|--------|------------|-----------| -| dev | Bool | | False | - -### `warcli cluster deploy-logging` -Deploy logging configurations to the cluster using helm - - -### `warcli cluster port-start` -Port forward (runs as a detached process) - - -### `warcli cluster port-stop` -Stop the port forwarding process - - -### `warcli cluster setup-minikube` -Configure a local minikube cluster - -options: -| name | type | required | default | -|--------|--------|------------|-----------| -| clean | Bool | | False | - -### `warcli cluster teardown` -Stop the warnet server and tear down the cluster - - -## Graph - -### `warcli graph create` -Create a cycle graph with \ nodes, and include 7 extra random outbounds per node. - Returns XML file as string with or without --outfile option - -options: -| name | type | required | default | -|--------------|--------|------------|-----------| -| number | Int | yes | | -| outfile | Path | | | -| version | String | | "27.0" | -| bitcoin_conf | Path | | | -| random | Bool | | False | - -### `warcli graph import-json` -Create a cycle graph with nodes imported from lnd `describegraph` JSON file, - and additionally include 7 extra random outbounds per node. Include lightning - channels and their policies as well. - Returns XML file as string with or without --outfile option. - -options: -| name | type | required | default | -|----------|--------|------------|-----------| -| infile | Path | yes | | -| outfile | Path | | | -| cb | String | | | -| ln_image | String | | | - -### `warcli graph validate` -Validate a \ against the schema. - -options: -| name | type | required | default | -|--------|--------|------------|-----------| -| graph | Path | yes | | - -## Image - -### `warcli image build` -Build bitcoind and bitcoin-cli from \ at \ as \:\. - Optionally deploy to remote registry using --action=push, otherwise image is loaded to local registry. - -options: -| name | type | required | default | -|------------|--------|------------|-----------| -| repo | String | yes | | -| commit_sha | String | yes | | -| registry | String | yes | | -| tag | String | yes | | -| build_args | String | | | -| arches | String | | | -| action | String | | "load" | - -## Ln - -### `warcli ln pubkey` -Get lightning node pub key on \ in [network] - -options: -| name | type | required | default | -|---------|--------|------------|-----------| -| node | Int | yes | | -| network | String | | "warnet" | - -### `warcli ln rpc` -Call lightning cli rpc \ on \ in [network] - -options: -| name | type | required | default | -|---------|--------|------------|-----------| -| node | Int | yes | | -| command | String | yes | | -| network | String | | "warnet" | - -## Network - -### `warcli network connected` -Indicate whether the all of the edges in the gaph file are connected in [network] - -options: -| name | type | required | default | -|---------|--------|------------|-----------| -| network | String | | "warnet" | - -### `warcli network down` -Bring down a running warnet named [network] - -options: -| name | type | required | default | -|---------|--------|------------|-----------| -| network | String | | "warnet" | - -### `warcli network export` -Export all [network] data for a "simln" service running in a container - on the network. Optionally add JSON string [activity] to simln config. - Optionally provide a list of tank indexes to [exclude]. - Returns True on success. - -options: -| name | type | required | default | -|----------|--------|------------|-----------| -| network | String | | "warnet" | -| activity | String | | | -| exclude | String | | "[]" | - -### `warcli network info` -Get info about a warnet named [network] - -options: -| name | type | required | default | -|---------|--------|------------|-----------| -| network | String | | "warnet" | - -### `warcli network logs` -Get Kubernetes logs from the RPC server - -options: -| name | type | required | default | -|--------|--------|------------|-----------| -| follow | Bool | | False | - -### `warcli network start` -Start a warnet with topology loaded from a \ into [network] - -options: -| name | type | required | default | -|------------|--------|------------|----------------------------------| -| graph_file | Path | | resources/graphs/default.graphml | -| force | Bool | | False | -| network | String | | "warnet" | - -### `warcli network status` -Get status of a warnet named [network] - -options: -| name | type | required | default | -|---------|--------|------------|-----------| -| network | String | | "warnet" | - -### `warcli network up` -Bring up a previously-stopped warnet named [network] - -options: -| name | type | required | default | -|---------|--------|------------|-----------| -| network | String | | "warnet" | - -## Scenarios - -### `warcli scenarios active` -List running scenarios "name": "pid" pairs - - -### `warcli scenarios available` -List available scenarios in the Warnet Test Framework - - -### `warcli scenarios run` -Run \ from the Warnet Test Framework on [network] with optional arguments - -options: -| name | type | required | default | -|-----------------|--------|------------|-----------| -| scenario | String | yes | | -| additional_args | String | | | -| network | String | | "warnet" | - -### `warcli scenarios run-file` -Run \ from the Warnet Test Framework on [network] with optional arguments - -options: -| name | type | required | default | -|-----------------|--------|------------|-----------| -| scenario_path | String | yes | | -| additional_args | String | | | -| name | String | | | -| network | String | | "warnet" | - -### `warcli scenarios stop` -Stop scenario with PID \ from running - -options: -| name | type | required | default | -|--------|--------|------------|-----------| -| pid | Int | yes | | - - diff --git a/docs/warnet.md b/docs/warnet.md new file mode 100644 index 000000000..2cba574ca --- /dev/null +++ b/docs/warnet.md @@ -0,0 +1,240 @@ +# `warnet` + +The command-line interface tool for Warnet. + +Once `warnet` is running it can be interacted with using the cli tool `warnet`. + +Execute `warnet --help` to see a list of command categories. + +Help text is provided, with optional parameters in [square brackets] and required +parameters in . + +`warnet` commands are organized in a hierarchy of categories and subcommands. + +## API Commands + +### `warnet auth` +Authenticate with a Warnet cluster using a kubernetes config file + +options: +| name | type | required | default | +|-------------|--------|------------|-----------| +| revert | Bool | | False | +| auth_config | String | | | + +### `warnet create` +Create a new warnet network + + +### `warnet dashboard` +Open the Warnet dashboard in default browser + + +### `warnet deploy` +Deploy a warnet with topology loaded from \ + +options: +| name | type | required | default | +|--------------|--------|------------|-----------| +| directory | Path | yes | | +| debug | Bool | | False | +| namespace | String | | | +| to_all_users | Bool | | False | + +### `warnet down` +Bring down a running warnet quickly + + +### `warnet import-network` +Create a network from an imported lightning network graph JSON + +options: +| name | type | required | default | +|-----------------|--------|------------|-----------| +| graph_file_path | Path | yes | | +| output_path | Path | yes | | + +### `warnet init` +Initialize a warnet project in the current directory + + +### `warnet logs` +Show the logs of a pod + +options: +| name | type | required | default | +|-----------|--------|------------|-----------| +| pod_name | String | | "" | +| follow | Bool | | False | +| namespace | String | | "default" | + +### `warnet new` +Create a new warnet project in the specified directory + +options: +| name | type | required | default | +|-----------|--------|------------|-----------| +| directory | Path | yes | | + +### `warnet run` +Run a scenario from a file. + Pass `-- --help` to get individual scenario help + +options: +| name | type | required | default | +|-----------------|--------|------------|-----------| +| scenario_file | Path | yes | | +| debug | Bool | | False | +| source_dir | Path | | | +| additional_args | String | | | +| admin | Bool | | False | +| namespace | String | | | + +### `warnet setup` +Setup warnet + + +### `warnet snapshot` +Create a snapshot of a tank's Bitcoin data or snapshot all tanks + +options: +| name | type | required | default | +|--------------|--------|------------|--------------------| +| tank_name | String | | | +| snapshot_all | Bool | | False | +| output | Path | | ./warnet-snapshots | +| filter | String | | | + +### `warnet status` +Display the unified status of the Warnet network and active scenarios + + +### `warnet stop` +Stop a running scenario or all scenarios + +options: +| name | type | required | default | +|---------------|--------|------------|-----------| +| scenario_name | String | | | + +### `warnet version` +Display the installed version of warnet + + +## Admin + +### `warnet admin create-kubeconfigs` +Create kubeconfig files for ServiceAccounts + +options: +| name | type | required | default | +|----------------|--------|------------|---------------| +| kubeconfig_dir | String | | "kubeconfigs" | +| token_duration | Int | | 172800 | + +### `warnet admin init` +Initialize a warnet project in the current directory + + +### `warnet admin namespaces` +Namespaces commands + + +## Bitcoin + +### `warnet bitcoin debug-log` +Fetch the Bitcoin Core debug log from \ + +options: +| name | type | required | default | +|-----------|--------|------------|-----------| +| tank | String | yes | | +| namespace | String | | | + +### `warnet bitcoin grep-logs` +Grep combined bitcoind logs using regex \ + +options: +| name | type | required | default | +|---------------------|--------|------------|-----------| +| pattern | String | yes | | +| show_k8s_timestamps | Bool | | False | +| no_sort | Bool | | False | + +### `warnet bitcoin messages` +Fetch messages sent between \ and \ in [chain] + + Optionally, include a namespace like so: tank-name.namespace + +options: +| name | type | required | default | +|--------|--------|------------|-----------| +| tank_a | String | yes | | +| tank_b | String | yes | | +| chain | String | | "regtest" | + +### `warnet bitcoin rpc` +Call bitcoin-cli \ [params] on \ + +options: +| name | type | required | default | +|-----------|--------|------------|-----------| +| tank | String | yes | | +| method | String | yes | | +| params | String | | | +| namespace | String | | | + +## Graph + +## Image + +### `warnet image build` +Build a Bitcoin Core Docker image with specified parameters. + +  + Usage Examples: + # Build an image for Warnet repository + warnet image build --repo bitcoin/bitcoin --commit-sha d6db87165c6dc2123a759c79ec236ea1ed90c0e3 --tags bitcoindevproject/bitcoin:v29.0-rc2 --arches amd64,arm64,armhf --action push + # Build an image for local testing + warnet image build --repo bitcoin/bitcoin --commit-sha d6db87165c6dc2123a759c79ec236ea1ed90c0e3 --tags bitcoindevproject/bitcoin:v29.0-rc2 --action load + +options: +| name | type | required | default | +|------------|--------|------------|-----------| +| repo | String | yes | | +| commit_sha | String | yes | | +| tags | String | yes | | +| build_args | String | | | +| arches | String | | | +| action | String | | "load" | + +## Ln + +### `warnet ln host` +Get lightning node host from \ + +options: +| name | type | required | default | +|--------|--------|------------|-----------| +| pod | String | yes | | + +### `warnet ln pubkey` +Get lightning node pub key from \ + +options: +| name | type | required | default | +|--------|--------|------------|-----------| +| pod | String | yes | | + +### `warnet ln rpc` +Call lightning cli rpc \ on \ + +options: +| name | type | required | default | +|-----------|--------|------------|-----------| +| pod | String | yes | | +| method | String | yes | | +| params | String | | | +| namespace | String | | | + + diff --git a/examples/networks/6_node_bitcoin/network.yaml b/examples/networks/6_node_bitcoin/network.yaml new file mode 100644 index 000000000..f27007931 --- /dev/null +++ b/examples/networks/6_node_bitcoin/network.yaml @@ -0,0 +1,37 @@ +nodes: + - name: tank-0001 + config: uacomment=tank0001 + image: + tag: "26.0" + addnode: + - tank-0002 + - tank-0003 + - name: tank-0002 + config: uacomment=tank0002 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + addnode: + - tank-0003 + - tank-0004 + - name: tank-0003 + config: uacomment=tank0003 + addnode: + - tank-0004 + - tank-0005 + - name: tank-0004 + config: uacomment=tank0004 + addnode: + - tank-0005 + - tank-0006 + - name: tank-0005 + config: uacomment=tank0005 + addnode: + - tank-0006 + - name: tank-0006 +caddy: + enabled: true diff --git a/examples/networks/6_node_bitcoin/node-defaults.yaml b/examples/networks/6_node_bitcoin/node-defaults.yaml new file mode 100644 index 000000000..3b0dabf61 --- /dev/null +++ b/examples/networks/6_node_bitcoin/node-defaults.yaml @@ -0,0 +1,26 @@ +chain: regtest + +collectLogs: true +metricsExport: true + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "27.0" + +defaultConfig: | + dns=1 + debug=rpc diff --git a/img/helm.svg b/img/helm.svg new file mode 100644 index 000000000..1e2db8a2e --- /dev/null +++ b/img/helm.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/kubernetes.svg b/img/kubernetes.svg new file mode 100644 index 000000000..bedd3b88e --- /dev/null +++ b/img/kubernetes.svg @@ -0,0 +1,84 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/pyproject.toml b/pyproject.toml index ef349b802..48d5d37f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,41 @@ [project] name = "warnet" -version = "0.10.0" +dynamic = ["version"] description = "Monitor and analyze the emergent behaviours of bitcoin networks" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.9" keywords = ["bitcoin", "warnet"] license = {text = "MIT"} classifiers = [ - "Programming Language :: Python :: 3", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +dependencies = [ + "click==8.1.7", + "docker==7.1.0", + "flask==3.0.3", + "inquirer==3.4.0", + "kubernetes==30.1.0", + "rich==13.7.1", + "tabulate==0.9.0", + "PyYAML==6.0.2", + "pexpect==4.9.0", ] -dynamic = ["dependencies"] [project.scripts] -warnet = "warnet.server:run_server" -warcli = "warnet.cli.main:cli" +warnet = "warnet.main:cli" +warcli = "warnet.main:cli" [project.urls] Homepage = "https://warnet.dev" -GitHub = "https://github.com/bitcoindevproject/warnet" +GitHub = "https://github.com/bitcoin-dev-project/warnet" +Pypi = "https://pypi.org/project/warnet/" [project.optional-dependencies] build = [ @@ -29,11 +47,17 @@ build = [ requires = ["setuptools>=64", "setuptools_scm>=8"] build-backend = "setuptools.build_meta" -[tool.setuptools.dynamic] -dependencies = {file = ["requirements.txt"]} - [tool.setuptools] include-package-data = true [tool.setuptools.packages.find] -where = ["src", "resources"] +where = ["src", ".", "resources/scenarios"] +include = ["warnet*", "test_framework*", "resources*"] + +[tool.setuptools.package-data] +"resources" = ["**/*"] + +[tool.setuptools_scm] +write_to = "src/warnet/_version.py" +version_scheme = "no-guess-dev" +local_scheme = "node-and-date" diff --git a/requirements.in b/requirements.in deleted file mode 100644 index ada695b50..000000000 --- a/requirements.in +++ /dev/null @@ -1,14 +0,0 @@ -click -docker -flask -Flask-JSONRPC -jsonschema -jsonrpcserver -jsonrpcclient -kubernetes -networkx -numpy -requests<2.30 -rich -tabulate -PyYAML diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 2455034f3..000000000 --- a/requirements.txt +++ /dev/null @@ -1,121 +0,0 @@ -# This file was autogenerated by uv via the following command: -# uv pip compile requirements.in -o requirements.txt -attrs==23.2.0 - # via - # jsonschema - # referencing -blinker==1.7.0 - # via flask -cachetools==5.3.2 - # via google-auth -certifi==2023.11.17 - # via - # kubernetes - # requests -charset-normalizer==3.3.2 - # via requests -click==8.1.7 - # via - # -r requirements.in - # flask -docker==7.0.0 - # via -r requirements.in -flask==3.0.0 - # via - # -r requirements.in - # flask-jsonrpc -flask-jsonrpc==1.1.0 - # via -r requirements.in -google-auth==2.25.2 - # via kubernetes -idna==3.6 - # via requests -itsdangerous==2.1.2 - # via flask -jinja2==3.1.2 - # via flask -jsonrpcclient==4.0.3 - # via -r requirements.in -jsonrpcserver==5.0.9 - # via -r requirements.in -jsonschema==4.20.0 - # via - # -r requirements.in - # jsonrpcserver -jsonschema-specifications==2023.12.1 - # via jsonschema -kubernetes==28.1.0 - # via -r requirements.in -markdown-it-py==3.0.0 - # via rich -markupsafe==2.1.3 - # via - # jinja2 - # werkzeug -mdurl==0.1.2 - # via markdown-it-py -networkx==3.2.1 - # via -r requirements.in -numpy==1.26.2 - # via -r requirements.in -oauthlib==3.2.2 - # via - # kubernetes - # requests-oauthlib -oslash==0.6.3 - # via jsonrpcserver -packaging==23.2 - # via docker -pyasn1==0.5.1 - # via - # pyasn1-modules - # rsa -pyasn1-modules==0.3.0 - # via google-auth -pygments==2.17.2 - # via rich -python-dateutil==2.8.2 - # via kubernetes -pyyaml==6.0.1 - # via - # -r requirements.in - # kubernetes -referencing==0.32.0 - # via - # jsonschema - # jsonschema-specifications -requests==2.29.0 - # via - # -r requirements.in - # docker - # kubernetes - # requests-oauthlib -requests-oauthlib==1.3.1 - # via kubernetes -rich==13.7.0 - # via -r requirements.in -rpds-py==0.16.2 - # via - # jsonschema - # referencing -rsa==4.9 - # via google-auth -six==1.16.0 - # via - # kubernetes - # python-dateutil -tabulate==0.9.0 - # via -r requirements.in -typeguard==2.13.3 - # via flask-jsonrpc -typing-extensions==4.9.0 - # via oslash -urllib3==1.26.18 - # via - # docker - # kubernetes - # requests -websocket-client==1.7.0 - # via kubernetes -werkzeug==3.0.1 - # via flask diff --git a/resources/graphs/__init__.py b/resources/charts/__init__.py similarity index 100% rename from resources/graphs/__init__.py rename to resources/charts/__init__.py diff --git a/resources/charts/bitcoincore/.helmignore b/resources/charts/bitcoincore/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/resources/charts/bitcoincore/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/resources/charts/bitcoincore/Chart.yaml b/resources/charts/bitcoincore/Chart.yaml new file mode 100644 index 000000000..36f7498af --- /dev/null +++ b/resources/charts/bitcoincore/Chart.yaml @@ -0,0 +1,32 @@ +apiVersion: v2 +name: bitcoincore +description: A Helm chart for Bitcoin Core + +dependencies: + - name: lnd + version: 0.1.0 + condition: ln.lnd + - name: cln + version: 0.1.0 + condition: ln.cln + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: 0.1.0 diff --git a/resources/charts/bitcoincore/charts/cln/.helmignore b/resources/charts/bitcoincore/charts/cln/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/resources/charts/bitcoincore/charts/cln/Chart.yaml b/resources/charts/bitcoincore/charts/cln/Chart.yaml new file mode 100644 index 000000000..79eeda983 --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: cln +description: A Helm chart for CLN + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "0.1.0" diff --git a/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl b/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl new file mode 100644 index 000000000..d983355cf --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/templates/_helpers.tpl @@ -0,0 +1,78 @@ +{{/* +Expand the name of the PARENT chart. +*/}} +{{- define "bitcoincore.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified PARENT app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bitcoincore.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + + +{{/* +Expand the name of the chart. +*/}} +{{- define "cln.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}-ln +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "cln.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }}-ln +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cln.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cln.labels" -}} +helm.sh/chart: {{ include "cln.chart" . }} +{{ include "cln.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cln.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cln.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "cln.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "cln.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml new file mode 100644 index 000000000..d3ff3b8ba --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/templates/configmap.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "cln.fullname" . }} + labels: + {{- include "cln.labels" . | nindent 4 }} +data: + config: | + {{- .Values.baseConfig | nindent 4 }} + {{- .Values.defaultConfig | nindent 4 }} + {{- .Values.config | nindent 4 }} + network={{ .Values.global.chain }} + addr=0.0.0.0:{{ .Values.P2PPort }} + bitcoin-rpcconnect={{ include "bitcoincore.fullname" . }} + bitcoin-rpcport={{ index .Values.global .Values.global.chain "RPCPort" }} + bitcoin-rpcpassword={{ .Values.global.rpcpassword }} + alias={{ include "cln.fullname" . }}.{{ .Release.Namespace }} + announce-addr=dns:{{ include "cln.fullname" . }}:{{ .Values.P2PPort }} + database-upgrade=true + bitcoin-retry-timeout=600 + grpc-port={{ .Values.RPCPort }} + grpc-host=0.0.0.0 + clnrest-host=0.0.0.0 + clnrest-port=3010 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "cln.fullname" . }}-channels + labels: + channels: "true" + {{- include "cln.labels" . | nindent 4 }} +data: + source: {{ include "cln.fullname" . }} + channels: | + {{ .Values.channels | toJson }} diff --git a/resources/charts/bitcoincore/charts/cln/templates/pod.yaml b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml new file mode 100644 index 000000000..59d212d0e --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/templates/pod.yaml @@ -0,0 +1,94 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "cln.fullname" . }} + labels: + {{- include "cln.labels" . | nindent 4 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + app: {{ include "cln.fullname" . }} + {{- if .Values.collectLogs }} + collect_logs: "true" + {{- end }} + chain: {{ .Values.global.chain }} + annotations: + kubectl.kubernetes.io/default-container: "cln" +spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 4 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 4 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 8 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: p2p + containerPort: {{ .Values.P2PPort }} + protocol: TCP + - name: rpc + containerPort: {{ .Values.RPCPort }} + protocol: TCP + - name: rest + containerPort: {{ .Values.RestPort }} + protocol: TCP + command: + - /bin/sh + - -c + - lightningd --conf=/root/.lightning/config + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 8 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 8 }} + startupProbe: + {{- toYaml .Values.startupProbe | nindent 8 }} + resources: + {{- toYaml .Values.resources | nindent 8 }} + volumeMounts: + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 8 }} + {{- end }} + - mountPath: /root/.lightning/config + name: config + subPath: config + {{- with .Values.extraContainers }} + {{- toYaml . | nindent 4 }} + {{- end }} + - name: http-server + image: busybox + command: ["/bin/sh", "-c"] + args: + - | + echo "Starting HTTP server..." + busybox httpd -f -p 8080 -h /working + ports: + - containerPort: 8080 + name: http + volumeMounts: + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + {{- with .Values.volumes }} + {{- toYaml . | nindent 4 }} + {{- end }} + - configMap: + name: {{ include "cln.fullname" . }} + name: config + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 4 }} + {{- end }} diff --git a/resources/charts/bitcoincore/charts/cln/templates/service.yaml b/resources/charts/bitcoincore/charts/cln/templates/service.yaml new file mode 100644 index 000000000..565f50182 --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/templates/service.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "cln.fullname" . }} + labels: + {{- include "cln.labels" . | nindent 4 }} + app: {{ include "cln.fullname" . }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.P2PPort }} + targetPort: p2p + protocol: TCP + name: p2p + - port: {{ .Values.RPCPort }} + targetPort: rpc + protocol: TCP + name: rpc + - port: {{ .Values.RestPort }} + targetPort: rest + protocol: TCP + name: rest + selector: + {{- include "cln.selectorLabels" . | nindent 4 }} diff --git a/resources/charts/bitcoincore/charts/cln/templates/servicemonitor.yaml b/resources/charts/bitcoincore/charts/cln/templates/servicemonitor.yaml new file mode 100644 index 000000000..60bc208f6 --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/templates/servicemonitor.yaml @@ -0,0 +1,16 @@ +{{- if .Values.metricsExport }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "cln.fullname" . }} + labels: + app.kubernetes.io/name: cln-metrics + release: prometheus +spec: + endpoints: + - port: prometheus-metrics + interval: {{ .Values.metricsScrapeInterval | default "15s" }} + selector: + matchLabels: + app: {{ include "cln.fullname" . }} +{{- end }} diff --git a/resources/charts/bitcoincore/charts/cln/values.yaml b/resources/charts/bitcoincore/charts/cln/values.yaml new file mode 100644 index 000000000..eaae7d2a3 --- /dev/null +++ b/resources/charts/bitcoincore/charts/cln/values.yaml @@ -0,0 +1,112 @@ +# Default values for cln. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +namespace: warnet + +image: + repository: elementsproject/lightningd + pullPolicy: IfNotPresent + tag: "v25.02" + +nameOverride: "" +fullnameOverride: "" + +podAnnotations: {} +podLabels: + app: "warnet" + mission: "lightning" + +podSecurityContext: {} + +securityContext: {} + +service: + type: ClusterIP + +P2PPort: 9735 +RPCPort: 9736 +RestPort: 3010 + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +livenessProbe: + exec: + command: + - "/bin/sh" + - "-c" + - "lightning-cli getinfo >/dev/null 2>&1" + failureThreshold: 3 + initialDelaySeconds: 5 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 +readinessProbe: + failureThreshold: 10 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 60 + exec: + command: + - "/bin/sh" + - "-c" + - "lightning-cli getinfo 2>/dev/null | grep -q 'id' || exit 1" +startupProbe: + failureThreshold: 10 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 60 + exec: + command: + - "/bin/sh" + - "-c" + - "lightning-cli createrune > /working/rune.json" + +# Additional volumes on the output Deployment definition. +volumes: + - name: working + emptyDir: {} + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: + - name: working + mountPath: "/working" + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +baseConfig: | + log-level=info + developer + dev-fast-gossip + bitcoin-rpcuser=user + # bitcoind.rpcpass are set in configmap.yaml + +config: "" + +defaultConfig: "" + +channels: [] diff --git a/resources/charts/bitcoincore/charts/lnd/Chart.yaml b/resources/charts/bitcoincore/charts/lnd/Chart.yaml new file mode 100644 index 000000000..b77eb714a --- /dev/null +++ b/resources/charts/bitcoincore/charts/lnd/Chart.yaml @@ -0,0 +1,25 @@ +apiVersion: v2 +name: lnd + +description: A Helm chart for LND + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: 0.1.0 diff --git a/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl b/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl new file mode 100644 index 000000000..c7f5ae792 --- /dev/null +++ b/resources/charts/bitcoincore/charts/lnd/templates/_helpers.tpl @@ -0,0 +1,87 @@ +{{/* +Expand the name of the PARENT chart. +*/}} +{{- define "bitcoincore.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified PARENT app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bitcoincore.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + + +{{/* +Expand the name of the chart. +*/}} +{{- define "lnd.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}-ln +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "lnd.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }}-ln +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "lnd.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "lnd.labels" -}} +helm.sh/chart: {{ include "lnd.chart" . }} +{{ include "lnd.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "lnd.selectorLabels" -}} +app.kubernetes.io/name: {{ include "lnd.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "lnd.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "lnd.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Create a hex-encoded RGB color derived from the namespace +*/}} +{{- define "namespace.color" -}} +{{- $hash := sha256sum .Release.Namespace -}} +{{- printf "#%s" (substr 0 6 $hash) -}} +{{- end -}} + diff --git a/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml b/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml new file mode 100644 index 000000000..8e5bb4069 --- /dev/null +++ b/resources/charts/bitcoincore/charts/lnd/templates/configmap.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "lnd.fullname" . }} + labels: + {{- include "lnd.labels" . | nindent 4 }} + role: macaroon-ref +data: + lnd.conf: | + color={{ include "namespace.color" . }} + {{- .Values.baseConfig | nindent 4 }} + {{- .Values.defaultConfig | nindent 4 }} + {{- .Values.config | nindent 4 }} + bitcoin.{{ .Values.global.chain }}=1 + bitcoind.rpcpass={{ .Values.global.rpcpassword }} + bitcoind.rpchost={{ include "bitcoincore.fullname" . }}:{{ index .Values.global .Values.global.chain "RPCPort" }} + bitcoind.zmqpubrawblock=tcp://{{ include "bitcoincore.fullname" . }}:{{ .Values.global.ZMQBlockPort }} + bitcoind.zmqpubrawtx=tcp://{{ include "bitcoincore.fullname" . }}:{{ .Values.global.ZMQTxPort }} + alias={{ include "lnd.fullname" . }}.{{ .Release.Namespace }} + externalhosts={{ include "lnd.fullname" . }} + tlsextradomain={{ include "lnd.fullname" . }} + tls.cert: | + -----BEGIN CERTIFICATE----- + MIIB+DCCAZ6gAwIBAgIUSbyK/9viFWS3cLoPkmxZsW8fcH8wCgYIKoZIzj0EAwIw + MjEfMB0GA1UECgwWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2Fy + bmV0MB4XDTI1MDkwMzE1NDgzNFoXDTM1MDkwMTE1NDgzNFowMjEfMB0GA1UECgwW + bG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2FybmV0MFkwEwYHKoZI + zj0CAQYIKoZIzj0DAQcDQgAENIGvS4bQr/zzUQnIqgJIYrPEdPMXVkv3yEyJRCFg + PyZTvxWUJy7AI3VKb7ubIXawYcnPBe7K1sgBAbTPz1c8sqOBkTCBjjAOBgNVHQ8B + Af8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAd + BgNVHQ4EFgQUNhDWW7rajlA9sNGI/1Q5BDLH/rMwNwYDVR0RBDAwLoIJbG9jYWxo + b3N0ggkqLmRlZmF1bHSHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwCgYIKoZIzj0E + AwIDSAAwRQIhAOFm85wvwPZMJg0+16Sh0FkKqAuGVmllHnriWHQJ1NhuAiAfoxzE + 9ooZuDwKy0Y3dP4DfJCrOlFNTHfp3abG7VQ+VQ== + -----END CERTIFICATE----- + + tls.key: | + -----BEGIN EC PRIVATE KEY----- + MHcCAQEEIEKlsxGkakClpHqXbr6tqEey634Xc364DgGMJxLdiLHIoAoGCCqGSM49 + AwEHoUQDQgAENIGvS4bQr/zzUQnIqgJIYrPEdPMXVkv3yEyJRCFgPyZTvxWUJy7A + I3VKb7ubIXawYcnPBe7K1sgBAbTPz1c8sg== + -----END EC PRIVATE KEY----- + + MACAROON_HEX: {{ .Values.adminMacaroon }} +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "lnd.fullname" . }}-channels + labels: + channels: "true" + {{- include "lnd.labels" . | nindent 4 }} +data: + source: {{ include "lnd.fullname" . }} + channels: | + {{ .Values.channels | toJson }} diff --git a/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml new file mode 100644 index 000000000..1b0305805 --- /dev/null +++ b/resources/charts/bitcoincore/charts/lnd/templates/pod.yaml @@ -0,0 +1,124 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "lnd.fullname" . }} + labels: + {{- include "lnd.labels" . | nindent 4 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + app: {{ include "lnd.fullname" . }} + {{- if .Values.collectLogs }} + collect_logs: "true" + {{- end }} + chain: {{ .Values.global.chain }} + annotations: + kubectl.kubernetes.io/default-container: "lnd" + adminMacaroon: {{ .Values.adminMacaroon }} +spec: + restartPolicy: "{{ .Values.restartPolicy }}" + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 4 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 4 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 8 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: rpc + containerPort: {{ .Values.RPCPort }} + protocol: TCP + - name: p2p + containerPort: {{ .Values.P2PPort }} + protocol: TCP + - name: rest + containerPort: {{ .Values.RestPort }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 8 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 8 }} + startupProbe: + failureThreshold: 10 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 300 + exec: + command: + - /bin/sh + - -c + - | + rm -rf /root/.lnd/data/chain + + until curl --silent --insecure https://localhost:8080/v1/genseed > /tmp/genseed.json; do + sleep 5 + done + + PHRASE=$(cat /tmp/genseed.json | grep -o '\[[^]]*\]') + + until curl --fail --insecure https://localhost:8080/v1/initwallet --data "{\"macaroon_root_key\":\"{{ .Values.macaroonRootKey }}\", \"wallet_password\":\"AAAAAAAAAAA=\", \"cipher_seed_mnemonic\": $PHRASE}"; do + sleep 5 + done + resources: + {{- toYaml .Values.resources | nindent 8 }} + volumeMounts: + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 8 }} + {{- end }} + - mountPath: /root/.lnd/lnd.conf + name: config + subPath: lnd.conf + - mountPath: /root/.lnd/tls.key + name: config + subPath: tls.key + - mountPath: /root/.lnd/tls.cert + name: config + subPath: tls.cert + - 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: Always + 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 }} + {{- end }} + - configMap: + name: {{ include "lnd.fullname" . }} + name: config + - name: shared-volume + emptyDir: {} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 4 }} + {{- end }} diff --git a/resources/charts/bitcoincore/charts/lnd/templates/service.yaml b/resources/charts/bitcoincore/charts/lnd/templates/service.yaml new file mode 100644 index 000000000..aecf301fe --- /dev/null +++ b/resources/charts/bitcoincore/charts/lnd/templates/service.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "lnd.fullname" . }} + labels: + {{- include "lnd.labels" . | nindent 4 }} + app: {{ include "lnd.fullname" . }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.RPCPort }} + targetPort: rpc + protocol: TCP + name: rpc + - port: {{ .Values.P2PPort }} + targetPort: p2p + protocol: TCP + name: p2p + - port: {{ .Values.RestPort }} + targetPort: rest + protocol: TCP + name: rest +{{- if .Values.metricsExport }} + - port: {{ .Values.prometheusMetricsPort }} + targetPort: prom-metrics + protocol: TCP + name: prometheus-metrics +{{- end }} + selector: + {{- include "lnd.selectorLabels" . | nindent 4 }} diff --git a/resources/charts/bitcoincore/charts/lnd/templates/servicemonitor.yaml b/resources/charts/bitcoincore/charts/lnd/templates/servicemonitor.yaml new file mode 100644 index 000000000..81ab31301 --- /dev/null +++ b/resources/charts/bitcoincore/charts/lnd/templates/servicemonitor.yaml @@ -0,0 +1,16 @@ +{{- if .Values.metricsExport }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "lnd.fullname" . }} + labels: + app.kubernetes.io/name: lnd-metrics + release: prometheus +spec: + endpoints: + - port: prometheus-metrics + interval: {{ .Values.metricsScrapeInterval | default "15s" }} + selector: + matchLabels: + app: {{ include "lnd.fullname" . }} +{{- end }} \ No newline at end of file diff --git a/resources/charts/bitcoincore/charts/lnd/values.yaml b/resources/charts/bitcoincore/charts/lnd/values.yaml new file mode 100644 index 000000000..28db1eb86 --- /dev/null +++ b/resources/charts/bitcoincore/charts/lnd/values.yaml @@ -0,0 +1,133 @@ +# Default values for lnd. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +namespace: warnet + +restartPolicy: Always + +image: + repository: lightninglabs/lnd + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "v0.19.0-beta" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +podLabels: + app: "warnet" + mission: "lightning" + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + +RPCPort: 10009 +P2PPort: 9735 +RestPort: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + + +livenessProbe: + exec: + command: + - pidof + - lnd + failureThreshold: 3 + initialDelaySeconds: 60 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 +readinessProbe: + initialDelaySeconds: 60 + failureThreshold: 1 + periodSeconds: 1 + successThreshold: 1 + tcpSocket: + port: 10009 + timeoutSeconds: 1 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +macaroonRootKey: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= +adminMacaroon: 0201036c6e6402f801030a1062beabbf2a614b112128afa0c0b4fdd61201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620b17be53e367290871681055d0de15587f6d1cd47d1248fe2662ae27f62cfbdc6 + +baseConfig: | + norest=false + restlisten=0.0.0.0:8080 + debuglevel=debug + accept-keysend=true + bitcoin.active=true + bitcoin.node=bitcoind + maxpendingchannels=64 + trickledelay=1 + rpclisten=0.0.0.0:10009 + bitcoind.rpcuser=user + protocol.wumbo-channels=1 + # zmq* and bitcoind.rpcpass are set in configmap.yaml + +config: "" + +defaultConfig: "" + +channels: [] + +circuitbreaker: + enabled: false # Default to disabled + image: carlakirkcohen/circuitbreaker:attackathon-test + httpPort: 9235 \ No newline at end of file diff --git a/resources/charts/bitcoincore/templates/NOTES.txt b/resources/charts/bitcoincore/templates/NOTES.txt new file mode 100644 index 000000000..a362b81c4 --- /dev/null +++ b/resources/charts/bitcoincore/templates/NOTES.txt @@ -0,0 +1 @@ +Thank you for installing {{ include "bitcoincore.fullname" . }}. \ No newline at end of file diff --git a/resources/charts/bitcoincore/templates/_helpers.tpl b/resources/charts/bitcoincore/templates/_helpers.tpl new file mode 100644 index 000000000..81ab85a37 --- /dev/null +++ b/resources/charts/bitcoincore/templates/_helpers.tpl @@ -0,0 +1,70 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "bitcoincore.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bitcoincore.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "bitcoincore.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "bitcoincore.labels" -}} +helm.sh/chart: {{ include "bitcoincore.chart" . }} +{{ include "bitcoincore.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "bitcoincore.selectorLabels" -}} +app.kubernetes.io/name: {{ include "bitcoincore.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "bitcoincore.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "bitcoincore.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + + +{{/* +Add network section heading in bitcoin.conf +Always add for custom semver, check version for valid semver +*/}} +{{- define "bitcoincore.check_semver" -}} +{{- $custom := contains "-" .Values.image.tag -}} +{{- $newer := semverCompare ">=0.17.0" .Values.image.tag -}} +{{- if or $newer $custom -}} +[{{ .Values.global.chain }}] +{{- end -}} +{{- end -}} diff --git a/resources/charts/bitcoincore/templates/configmap.yaml b/resources/charts/bitcoincore/templates/configmap.yaml new file mode 100644 index 000000000..cc1e580f2 --- /dev/null +++ b/resources/charts/bitcoincore/templates/configmap.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "bitcoincore.fullname" . }} + labels: + {{- include "bitcoincore.labels" . | nindent 4 }} +data: + bitcoin.conf: | + {{ .Values.global.chain }}=1 + + {{ template "bitcoincore.check_semver" . }} + {{- .Values.baseConfig | nindent 4 }} + rpcport={{ index .Values.global .Values.global.chain "RPCPort" }} + rpcpassword={{ .Values.global.rpcpassword }} + zmqpubrawblock=tcp://0.0.0.0:{{ .Values.global.ZMQBlockPort }} + zmqpubrawtx=tcp://0.0.0.0:{{ .Values.global.ZMQTxPort }} + {{- .Values.defaultConfig | nindent 4 }} + {{- .Values.config | nindent 4 }} + {{- range .Values.addnode }} + {{- print "addnode=" . | nindent 4}} + {{- end }} diff --git a/resources/charts/bitcoincore/templates/pod.yaml b/resources/charts/bitcoincore/templates/pod.yaml new file mode 100644 index 000000000..f8b8d1de1 --- /dev/null +++ b/resources/charts/bitcoincore/templates/pod.yaml @@ -0,0 +1,127 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "bitcoincore.fullname" . }} + labels: + {{- include "bitcoincore.labels" . | nindent 4 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + chain: {{ .Values.global.chain }} + P2PPort: "{{ index .Values.global .Values.global.chain "P2PPort" }}" + RPCPort: "{{ index .Values.global .Values.global.chain "RPCPort" }}" + ZMQTxPort: "{{ .Values.global.ZMQTxPort }}" + ZMQBlockPort: "{{ .Values.global.ZMQBlockPort }}" + rpcpassword: {{ .Values.global.rpcpassword }} + app: {{ include "bitcoincore.fullname" . }} + {{- if .Values.collectLogs }} + collect_logs: "true" + {{- end }} + annotations: + init_peers: "{{ .Values.addnode | len }}" +spec: + restartPolicy: "{{ .Values.restartPolicy }}" + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 4 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 4 }} + {{- if .Values.loadSnapshot.enabled }} + initContainers: + - name: download-blocks + image: alpine:latest + command: ["/bin/sh", "-c"] + args: + - | + apk add --no-cache curl + mkdir -p /root/.bitcoin/{{ .Values.global.chain }} + curl -L {{ .Values.loadSnapshot.url }} | tar -xz -C /root/.bitcoin/{{ .Values.global.chain }} + volumeMounts: + - name: data + mountPath: /root/.bitcoin + {{- end }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 8 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: rpc + containerPort: {{ index .Values.global .Values.global.chain "RPCPort" }} + protocol: TCP + - name: p2p + containerPort: {{ index .Values.global .Values.global.chain "P2PPort" }} + protocol: TCP + - name: zmq-tx + containerPort: {{ .Values.global.ZMQTxPort }} + protocol: TCP + - name: zmq-block + containerPort: {{ .Values.global.ZMQBlockPort }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 8 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 8 }} + tcpSocket: + port: {{ index .Values.global .Values.global.chain "RPCPort" }} + startupProbe: + {{- toYaml .Values.startupProbe | nindent 8 }} + resources: + {{- toYaml .Values.resources | nindent 8 }} + volumeMounts: + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 8 }} + {{- end }} + - mountPath: /root/.bitcoin + name: data + - mountPath: /root/.bitcoin/bitcoin.conf + name: config + subPath: bitcoin.conf + {{- if .Values.metricsExport }} + - name: prometheus + image: bitcoindevproject/bitcoin-exporter:latest + imagePullPolicy: IfNotPresent + ports: + - name: prom-metrics + containerPort: {{ .Values.prometheusMetricsPort }} + protocol: TCP + env: + - name: BITCOIN_RPC_HOST + value: "127.0.0.1" + - name: BITCOIN_RPC_PORT + value: "{{ index .Values.global .Values.global.chain "RPCPort" }}" + - name: BITCOIN_RPC_USER + value: user + - name: BITCOIN_RPC_PASSWORD + value: {{ .Values.global.rpcpassword }} + {{- if .Values.metrics }} + - name: METRICS + value: {{ .Values.metrics }} + {{- end }} + {{- end}} + {{- with .Values.extraContainers }} + {{- toYaml . | nindent 4 }} + {{- end }} + volumes: + {{- with .Values.volumes }} + {{- toYaml . | nindent 4 }} + {{- end }} + - name: data + emptyDir: {} + - name: config + configMap: + name: {{ include "bitcoincore.fullname" . }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 4 }} + {{- end }} diff --git a/resources/charts/bitcoincore/templates/service.yaml b/resources/charts/bitcoincore/templates/service.yaml new file mode 100644 index 000000000..8d8fa5324 --- /dev/null +++ b/resources/charts/bitcoincore/templates/service.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "bitcoincore.fullname" . }} + labels: + {{- include "bitcoincore.labels" . | nindent 4 }} + app: {{ include "bitcoincore.fullname" . }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ index .Values.global .Values.global.chain "RPCPort" }} + targetPort: rpc + protocol: TCP + name: rpc + - port: {{ index .Values.global .Values.global.chain "P2PPort" }} + targetPort: p2p + protocol: TCP + name: p2p + - port: {{ .Values.global.ZMQTxPort }} + targetPort: zmq-tx + protocol: TCP + name: zmq-tx + - port: {{ .Values.global.ZMQBlockPort }} + targetPort: zmq-block + protocol: TCP + name: zmq-block + - port: {{ .Values.prometheusMetricsPort }} + targetPort: prom-metrics + protocol: TCP + name: prometheus-metrics + selector: + {{- include "bitcoincore.selectorLabels" . | nindent 4 }} diff --git a/resources/charts/bitcoincore/templates/servicemonitor.yaml b/resources/charts/bitcoincore/templates/servicemonitor.yaml new file mode 100644 index 000000000..552b92c30 --- /dev/null +++ b/resources/charts/bitcoincore/templates/servicemonitor.yaml @@ -0,0 +1,16 @@ +{{- if .Values.metricsExport }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "bitcoincore.fullname" . }} + labels: + app.kubernetes.io/name: bitcoind-metrics + release: prometheus +spec: + endpoints: + - port: prometheus-metrics + interval: {{ .Values.metricsScrapeInterval | default "15s" }} + selector: + matchLabels: + app: {{ include "bitcoincore.fullname" . }} +{{- end }} \ No newline at end of file diff --git a/resources/charts/bitcoincore/values.yaml b/resources/charts/bitcoincore/values.yaml new file mode 100644 index 000000000..67f1b9b10 --- /dev/null +++ b/resources/charts/bitcoincore/values.yaml @@ -0,0 +1,145 @@ +# Default values for bitcoincore. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +namespace: warnet + +restartPolicy: Never + +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "27.0" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +podLabels: + app: "warnet" + mission: "tank" + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + + +livenessProbe: + exec: + command: + - pidof + - bitcoind + failureThreshold: 12 + initialDelaySeconds: 5 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 10 +readinessProbe: + failureThreshold: 12 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 10 + + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +collectLogs: false +metricsExport: false +prometheusMetricsPort: 9332 + +# These are values that are propogated to the sub-charts (i.e. lightning nodes) +global: + chain: regtest + regtest: + RPCPort: 18443 + P2PPort: 18444 + signet: + RPCPort: 38332 + P2PPort: 38333 + ZMQTxPort: 28333 + ZMQBlockPort: 28332 + rpcpassword: gn0cchi + +baseConfig: | + checkmempool=0 + debuglogfile=debug.log + logips=1 + logtimemicros=1 + capturemessages=1 + fallbackfee=0.00001000 + listen=1 + rpcuser=user + # rpcpassword MUST be set as a chart value + rpcallowip=0.0.0.0/0 + rpcbind=0.0.0.0 + rest=1 + # rpcport and zmq endpoints are configured by chain in configmap.yaml + +config: "" + +defaultConfig: "" + +addnode: [] + +loadSnapshot: + enabled: false + url: "" + +ln: + lnd: false + cln: false diff --git a/resources/charts/caddy/.helmignore b/resources/charts/caddy/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/resources/charts/caddy/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/resources/charts/caddy/Chart.yaml b/resources/charts/caddy/Chart.yaml new file mode 100644 index 000000000..4fbb87241 --- /dev/null +++ b/resources/charts/caddy/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: caddy-server +description: A Helm chart for Caddy + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: 0.1.0 diff --git a/resources/charts/caddy/templates/NOTES.txt b/resources/charts/caddy/templates/NOTES.txt new file mode 100644 index 000000000..9d2cc4cf0 --- /dev/null +++ b/resources/charts/caddy/templates/NOTES.txt @@ -0,0 +1 @@ +Caddy is serving your every need. diff --git a/resources/charts/caddy/templates/_helpers.tpl b/resources/charts/caddy/templates/_helpers.tpl new file mode 100644 index 000000000..7cfc3d479 --- /dev/null +++ b/resources/charts/caddy/templates/_helpers.tpl @@ -0,0 +1,57 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "caddy.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "caddy.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "caddy.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "caddy.labels" -}} +helm.sh/chart: {{ include "caddy.chart" . }} +{{ include "caddy.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "caddy.selectorLabels" -}} +app.kubernetes.io/name: {{ include "caddy.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "caddy.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "caddy.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/resources/charts/caddy/templates/configmap.yaml b/resources/charts/caddy/templates/configmap.yaml new file mode 100644 index 000000000..e56729dfe --- /dev/null +++ b/resources/charts/caddy/templates/configmap.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "caddy.fullname" . }} + labels: + {{- include "caddy.labels" . | nindent 4 }} +data: + Caddyfile: | + :80 { + respond /live 200 + respond /ready 200 + + root * /usr/share/caddy + file_server + + {{- range .Values.services }} + handle_path {{ .path }}* { + reverse_proxy {{ .host }}:{{ .port }} + } + {{- end }} + } + index: | + + + + + + Warnet Dashboard + + +

Welcome to the Warnet dashboard

+

You can access the following services:

+
    + {{- range .Values.services }} +
  • {{ .title }}
  • + {{- end }} +
+ + diff --git a/resources/charts/caddy/templates/ingress.yaml b/resources/charts/caddy/templates/ingress.yaml new file mode 100644 index 000000000..79c9ca105 --- /dev/null +++ b/resources/charts/caddy/templates/ingress.yaml @@ -0,0 +1,18 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: caddy-ingress + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "false" +spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ include "caddy.fullname" . }} + port: + number: {{ .Values.port }} \ No newline at end of file diff --git a/resources/charts/caddy/templates/pod.yaml b/resources/charts/caddy/templates/pod.yaml new file mode 100644 index 000000000..6e034934a --- /dev/null +++ b/resources/charts/caddy/templates/pod.yaml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "caddy.fullname" . }} + labels: + {{- include "caddy.labels" . | nindent 4 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + app: {{ include "caddy.fullname" . }} +spec: + restartPolicy: "{{ .Values.restartPolicy }}" + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 4 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 4 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 8 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: web + containerPort: {{ .Values.port }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 8 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 8 }} + resources: + {{- toYaml .Values.resources | nindent 8 }} + volumeMounts: + {{- toYaml .Values.volumeMounts | nindent 8 }} + volumes: + {{- toYaml .Values.volumes | nindent 4 }} diff --git a/resources/charts/caddy/templates/service.yaml b/resources/charts/caddy/templates/service.yaml new file mode 100644 index 000000000..a25c46946 --- /dev/null +++ b/resources/charts/caddy/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "caddy.fullname" . }} + labels: + {{- include "caddy.labels" . | nindent 4 }} + app: {{ include "caddy.fullname" . }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.port }} + targetPort: web + protocol: TCP + name: http + selector: + {{- include "caddy.selectorLabels" . | nindent 4 }} diff --git a/resources/charts/caddy/values.yaml b/resources/charts/caddy/values.yaml new file mode 100644 index 000000000..d03151eb0 --- /dev/null +++ b/resources/charts/caddy/values.yaml @@ -0,0 +1,87 @@ +# Default values for caddy. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +namespace: warnet + +restartPolicy: Always + +image: + repository: caddy + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "2.8.4" + +imagePullSecrets: [] + +nameOverride: "" + +fullnameOverride: "" + +podLabels: + app: "warnet" + mission: "proxy" + +podSecurityContext: {} +# fsGroup: 2000 + +securityContext: {} +# capabilities: +# drop: +# - ALL +# readOnlyRootFilesystem: true +# runAsNonRoot: true +# runAsUser: 1000 + +service: + type: ClusterIP + +resources: {} +# We usually recommend not to specify default resources and to leave this as a conscious +# choice for the user. This also increases chances charts run on environments with little +# resources, such as Minikube. If you do want to specify resources, uncomment the following +# lines, adjust them as necessary, and remove the curly braces after 'resources:'. +# limits: +# cpu: 100m +# memory: 128Mi +# requests: +# cpu: 100m +# memory: 128Mi + +livenessProbe: + httpGet: + path: /live + port: 80 + failureThreshold: 3 + initialDelaySeconds: 5 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 + +readinessProbe: + httpGet: + path: /ready + port: 80 + failureThreshold: 1 + periodSeconds: 1 + successThreshold: 1 + timeoutSeconds: 1 + +volumes: + - name: caddy-config + configMap: + name: caddy + items: + - key: Caddyfile + path: Caddyfile + - key: index + path: index + +volumeMounts: + - name: caddy-config + mountPath: /etc/caddy/Caddyfile + subPath: Caddyfile + - name: caddy-config + mountPath: /usr/share/caddy/index.html + subPath: index + +port: 80 diff --git a/resources/charts/commander/.helmignore b/resources/charts/commander/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/resources/charts/commander/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/resources/charts/commander/Chart.yaml b/resources/charts/commander/Chart.yaml new file mode 100644 index 000000000..202456e92 --- /dev/null +++ b/resources/charts/commander/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: commander +description: A Helm chart for a commander + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: 0.1.0 diff --git a/resources/charts/commander/templates/NOTES.txt b/resources/charts/commander/templates/NOTES.txt new file mode 100644 index 000000000..29639a44e --- /dev/null +++ b/resources/charts/commander/templates/NOTES.txt @@ -0,0 +1 @@ +Commander beginning their mission. diff --git a/resources/charts/commander/templates/_helpers.tpl b/resources/charts/commander/templates/_helpers.tpl new file mode 100644 index 000000000..9383f0ff9 --- /dev/null +++ b/resources/charts/commander/templates/_helpers.tpl @@ -0,0 +1,60 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "commander.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "commander.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "commander.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "commander.labels" -}} +helm.sh/chart: {{ include "commander.chart" . }} +{{ include "commander.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- with .Values.podLabels }} +{{ toYaml . }} +{{- end }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "commander.selectorLabels" -}} +app.kubernetes.io/name: {{ include "commander.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "commander.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "commander.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/resources/charts/commander/templates/pod.yaml b/resources/charts/commander/templates/pod.yaml new file mode 100644 index 000000000..0ad4583e1 --- /dev/null +++ b/resources/charts/commander/templates/pod.yaml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "commander.fullname" . }} + labels: + {{- include "commander.labels" . | nindent 4 }} + app: {{ include "commander.name" . }} + mission: commander +spec: + restartPolicy: {{ .Values.restartPolicy }} + initContainers: + - name: init + image: busybox + command: ["/bin/sh", "-c"] + args: + - | + while [ ! -f /shared/archive.pyz ]; do + echo "Waiting for /shared/archive.pyz to exist..." + sleep 2 + done + volumeMounts: + - name: shared-volume + mountPath: /shared + containers: + - name: {{ .Chart.Name }} + image: bitcoindevproject/commander + imagePullPolicy: IfNotPresent + command: ["/bin/sh", "-c"] + args: + - | + python3 /shared/archive.pyz {{ .Values.args }} + volumeMounts: + - name: shared-volume + mountPath: /shared + volumes: + - name: shared-volume + emptyDir: {} + serviceAccountName: {{ include "commander.fullname" . }} \ No newline at end of file diff --git a/resources/charts/commander/templates/rbac.yaml b/resources/charts/commander/templates/rbac.yaml new file mode 100644 index 000000000..d3d62b77d --- /dev/null +++ b/resources/charts/commander/templates/rbac.yaml @@ -0,0 +1,65 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "commander.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ .Chart.Name }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "commander.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ .Chart.Name }} +rules: + - apiGroups: [""] + resources: ["pods", "configmaps"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "commander.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ .Chart.Name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "commander.fullname" . }} +subjects: + - kind: ServiceAccount + name: {{ include "commander.fullname" . }} + namespace: {{ .Release.Namespace }} +{{- if .Values.admin }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "commander.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ .Chart.Name }} +rules: + - apiGroups: [""] + resources: ["pods", "namespaces", "configmaps", "pods/log", "pods/exec"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "commander.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + app.kubernetes.io/name: {{ .Chart.Name }} +roleRef: + kind: ClusterRole + name: {{ include "commander.fullname" . }} + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: {{ include "commander.fullname" . }} + namespace: {{ .Release.Namespace }} +{{- end}} \ No newline at end of file diff --git a/resources/charts/commander/values.yaml b/resources/charts/commander/values.yaml new file mode 100644 index 000000000..23ba35354 --- /dev/null +++ b/resources/charts/commander/values.yaml @@ -0,0 +1,70 @@ +# Default values for commander. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +namespace: warnet + +restartPolicy: Never + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +podLabels: + app: "warnet" + mission: "commander" + +podSecurityContext: {} + +securityContext: {} + +service: + type: ClusterIP + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# livenessProbe: +# exec: +# command: +# - pidof +# - commander +# failureThreshold: 3 +# initialDelaySeconds: 5 +# periodSeconds: 5 +# successThreshold: 1 +# timeoutSeconds: 1 +# readinessProbe: +# failureThreshold: 1 +# periodSeconds: 1 +# successThreshold: 1 +# tcpSocket: +# port: 2323 +# timeoutSeconds: 1 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +port: + +args: "" + +admin: false \ No newline at end of file diff --git a/resources/charts/fork-observer/.helmignore b/resources/charts/fork-observer/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/resources/charts/fork-observer/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/resources/charts/fork-observer/Chart.yaml b/resources/charts/fork-observer/Chart.yaml new file mode 100644 index 000000000..a8676c16f --- /dev/null +++ b/resources/charts/fork-observer/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: fork-observer +description: A Helm chart for fork-observer + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: 0.1.0 diff --git a/resources/charts/fork-observer/templates/NOTES.txt b/resources/charts/fork-observer/templates/NOTES.txt new file mode 100644 index 000000000..36a37bdc8 --- /dev/null +++ b/resources/charts/fork-observer/templates/NOTES.txt @@ -0,0 +1 @@ +Fork observer enabled. diff --git a/resources/charts/fork-observer/templates/_helpers.tpl b/resources/charts/fork-observer/templates/_helpers.tpl new file mode 100644 index 000000000..8ff2e9aed --- /dev/null +++ b/resources/charts/fork-observer/templates/_helpers.tpl @@ -0,0 +1,57 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "fork-observer.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "fork-observer.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "fork-observer.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "fork-observer.labels" -}} +helm.sh/chart: {{ include "fork-observer.chart" . }} +{{ include "fork-observer.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "fork-observer.selectorLabels" -}} +app.kubernetes.io/name: {{ include "fork-observer.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "fork-observer.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "fork-observer.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/resources/charts/fork-observer/templates/configmap.yaml b/resources/charts/fork-observer/templates/configmap.yaml new file mode 100644 index 000000000..f1fc97930 --- /dev/null +++ b/resources/charts/fork-observer/templates/configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "fork-observer.fullname" . }} + labels: + {{- include "fork-observer.labels" . | nindent 4 }} +data: + config.toml: | + {{- .Values.configQueryInterval | nindent 4 }} + {{- tpl .Values.baseConfig . | nindent 4 }} + {{- .Values.config | nindent 8 }} diff --git a/resources/charts/fork-observer/templates/pod.yaml b/resources/charts/fork-observer/templates/pod.yaml new file mode 100644 index 000000000..908543806 --- /dev/null +++ b/resources/charts/fork-observer/templates/pod.yaml @@ -0,0 +1,48 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "fork-observer.fullname" . }} + labels: + {{- include "fork-observer.labels" . | nindent 4 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 4 }} + {{- end }} + app: {{ include "fork-observer.fullname" . }} +spec: + restartPolicy: "{{ .Values.restartPolicy }}" + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 4 }} + {{- end }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 4 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 8 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: web + containerPort: {{ .Values.port }} + protocol: TCP + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 8 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 8 }} + resources: + {{- toYaml .Values.resources | nindent 8 }} + volumeMounts: + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 8 }} + {{- end }} + - mountPath: /app/config.toml + name: config + subPath: config.toml + volumes: + {{- with .Values.volumes }} + {{- toYaml . | nindent 4 }} + {{- end }} + - configMap: + name: {{ include "fork-observer.fullname" . }} + name: config diff --git a/resources/charts/fork-observer/templates/service.yaml b/resources/charts/fork-observer/templates/service.yaml new file mode 100644 index 000000000..91615e15c --- /dev/null +++ b/resources/charts/fork-observer/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "fork-observer.fullname" . }} + labels: + {{- include "fork-observer.labels" . | nindent 4 }} + app: {{ include "fork-observer.fullname" . }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.port }} + targetPort: web + protocol: TCP + name: rpc + selector: + {{- include "fork-observer.selectorLabels" . | nindent 4 }} diff --git a/resources/charts/fork-observer/values.yaml b/resources/charts/fork-observer/values.yaml new file mode 100644 index 000000000..a6543de7c --- /dev/null +++ b/resources/charts/fork-observer/values.yaml @@ -0,0 +1,115 @@ +# Default values for fork-observer. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +namespace: warnet + +restartPolicy: Always + +image: + repository: b10c/fork-observer + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "latest" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +podLabels: + app: "warnet" + mission: "observer" + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +livenessProbe: + exec: + command: + - pidof + - fork-observer + failureThreshold: 3 + initialDelaySeconds: 5 + periodSeconds: 5 + successThreshold: 1 + timeoutSeconds: 1 +readinessProbe: + failureThreshold: 1 + periodSeconds: 1 + successThreshold: 1 + tcpSocket: + port: 2323 + timeoutSeconds: 1 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +port: 2323 + +configQueryInterval: | + # Interval for checking for new blocks + query_interval = 20 + +maxInterestingHeights: 100 + +baseConfig: | + # Database path of the key value store. Will be created if non-existing. + database_path = "db" + + # path to the location of the static www files + www_path = "./www" + + # Webserver listen address + address = "0.0.0.0:2323" + + # Custom footer for the site. + footer_html = """ +
+
+ Warnet fork-observer +
+
+ """ + + [[networks]] + id = 0xDEADBE + name = "Warnet" + description = "A Warnet" + min_fork_height = 0 + max_interesting_heights = {{ .Values.maxInterestingHeights }} + [pool_identification] + enable = false + +config: "" diff --git a/resources/charts/grafana-dashboards/.helmignore b/resources/charts/grafana-dashboards/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/resources/charts/grafana-dashboards/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/resources/charts/grafana-dashboards/Chart.yaml b/resources/charts/grafana-dashboards/Chart.yaml new file mode 100644 index 000000000..a1e7b5605 --- /dev/null +++ b/resources/charts/grafana-dashboards/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: grafana-dashboards +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/resources/configs/grafana/default_dashboard.json b/resources/charts/grafana-dashboards/files/default_dashboard.json similarity index 95% rename from resources/configs/grafana/default_dashboard.json rename to resources/charts/grafana-dashboards/files/default_dashboard.json index 0ed627e22..97cb02245 100644 --- a/resources/configs/grafana/default_dashboard.json +++ b/resources/charts/grafana-dashboards/files/default_dashboard.json @@ -24,7 +24,8 @@ ], "title": "Outbound connections", "type": "timeseries", - "gridPos": { + "gridPos": + { "h": 8, "w": 24, "x": 0, @@ -52,7 +53,8 @@ ], "title": "Inbound connections", "type": "timeseries", - "gridPos": { + "gridPos": + { "h": 8, "w": 24, "x": 0, @@ -80,7 +82,8 @@ ], "title": "Mempool size", "type": "timeseries", - "gridPos": { + "gridPos": + { "h": 8, "w": 24, "x": 0, @@ -108,7 +111,8 @@ ], "title": "Blocks", "type": "timeseries", - "gridPos": { + "gridPos": + { "h": 8, "w": 24, "x": 0, diff --git a/resources/charts/grafana-dashboards/files/sample_lnd_dashboard.json b/resources/charts/grafana-dashboards/files/sample_lnd_dashboard.json new file mode 100644 index 000000000..8a991e6ae --- /dev/null +++ b/resources/charts/grafana-dashboards/files/sample_lnd_dashboard.json @@ -0,0 +1,91 @@ +{ + "title": "Sample Warnet Dashboard (LND)", + "refresh": "5s", + "time": { + "from": "now-30m", + "to": "now" + }, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2094" + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2094" + }, + "editorMode": "code", + "expr": "lnd_balance_channels", + "legendFormat": "{{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "Channel Balance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2094" + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 8 + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2094" + }, + "editorMode": "code", + "expr": "lnd_peers", + "legendFormat": "{{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "Peers", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2094" + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 16 + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2094" + }, + "editorMode": "code", + "expr": "lnd_block_height", + "legendFormat": "{{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "LND Blocks", + "type": "timeseries" + } + ] +} \ No newline at end of file diff --git a/resources/charts/grafana-dashboards/templates/NOTES.txt b/resources/charts/grafana-dashboards/templates/NOTES.txt new file mode 100644 index 000000000..33048e5f0 --- /dev/null +++ b/resources/charts/grafana-dashboards/templates/NOTES.txt @@ -0,0 +1 @@ +Grafana dashboards deployed \ No newline at end of file diff --git a/resources/charts/grafana-dashboards/templates/configmap.yaml b/resources/charts/grafana-dashboards/templates/configmap.yaml new file mode 100644 index 000000000..564c3f615 --- /dev/null +++ b/resources/charts/grafana-dashboards/templates/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboards-config +data: +{{- $files := .Files.Glob "files/*.json" }} +{{- range $path, $file := $files }} + {{ base $path }}: |- +{{ $file | toString | indent 4 }} +{{- end }} diff --git a/resources/charts/namespaces/Chart.yaml b/resources/charts/namespaces/Chart.yaml new file mode 100644 index 000000000..d023219e0 --- /dev/null +++ b/resources/charts/namespaces/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: battalion-namespace +description: A Helm chart for creating a battalion namespace +type: application +version: 0.1.0 +appVersion: "1.0.0" diff --git a/resources/charts/namespaces/templates/namespace.yaml b/resources/charts/namespaces/templates/namespace.yaml new file mode 100644 index 000000000..bcfd7e38c --- /dev/null +++ b/resources/charts/namespaces/templates/namespace.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .Values.namespaceName | default .Release.Name }} +--- +apiVersion: v1 +kind: ResourceQuota +metadata: + name: namespace-pod-limit + namespace: {{ .Values.namespaceName | default .Release.Name }} +spec: + hard: + pods: {{ .Values.podLimit | quote }} diff --git a/resources/charts/namespaces/templates/role.yaml b/resources/charts/namespaces/templates/role.yaml new file mode 100644 index 000000000..1acfe80f8 --- /dev/null +++ b/resources/charts/namespaces/templates/role.yaml @@ -0,0 +1,9 @@ +{{- range .Values.roles }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .name }}-{{ $.Release.Name }} +rules: +{{ toYaml .rules | indent 2 }} +{{- end }} diff --git a/resources/charts/namespaces/templates/rolebinding.yaml b/resources/charts/namespaces/templates/rolebinding.yaml new file mode 100644 index 000000000..374aa502f --- /dev/null +++ b/resources/charts/namespaces/templates/rolebinding.yaml @@ -0,0 +1,22 @@ +{{- range $user := .Values.users }} +{{- range $roleName := $user.roles }} +{{- range $r := $.Values.roles }} +{{- if eq $r.name $roleName }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ $.Release.Name }}-{{ $roleName }}-{{ $user.name }} + namespace: {{ $r.namespaceName | default $.Values.namespaceName | default $.Release.Name }} +subjects: +- kind: ServiceAccount + name: {{ $user.name }} + namespace: {{ $.Values.namespaceName | default $.Release.Name }} +roleRef: + kind: ClusterRole + name: {{ $roleName }}-{{ $.Release.Name }} + apiGroup: rbac.authorization.k8s.io +{{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/resources/charts/namespaces/templates/serviceaccount.yaml b/resources/charts/namespaces/templates/serviceaccount.yaml new file mode 100644 index 000000000..dd325929d --- /dev/null +++ b/resources/charts/namespaces/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- range .Values.users }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .name }} + namespace: {{ $.Values.namespaceName | default $.Release.Name }} + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-weight: "-5" + labels: + mission: user +{{- end }} diff --git a/resources/charts/namespaces/values.yaml b/resources/charts/namespaces/values.yaml new file mode 100644 index 000000000..26012ca48 --- /dev/null +++ b/resources/charts/namespaces/values.yaml @@ -0,0 +1,61 @@ +users: + - name: warnet-user + roles: + - pod-viewer + - pod-manager + - ingress-viewer + - ingress-controller-viewer +roles: + - name: pod-viewer + rules: + - apiGroups: [""] + resources: ["pods", "services", "configmaps"] + verbs: ["get", "list", "watch"] + - apiGroups: [""] + resources: ["pods/log", "pods/exec", "pods/attach", "pods/portforward"] + verbs: ["get"] + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["persistentvolumeclaims", "namespaces"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["events", "pods/status"] + verbs: ["get"] + - name: pod-manager + rules: + - apiGroups: [""] + resources: ["pods", "services"] + verbs: ["get", "list", "watch", "create", "delete", "update"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch", "create", "delete", "update", "patch"] + - apiGroups: [""] + resources: ["pods/log", "pods/exec", "pods/attach", "pods/portforward"] + verbs: ["get", "create"] + - apiGroups: [""] + resources: ["configmaps", "secrets", "serviceaccounts"] + verbs: ["get", "list", "create", "update", "watch", "patch"] + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles", "rolebindings"] + verbs: ["get", "list", "create", "update", "patch"] + - apiGroups: [""] + resources: ["persistentvolumeclaims", "namespaces"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["events", "pods/status"] + verbs: ["get"] + - name: ingress-viewer + namespaceName: ingress + rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["list", "get", "watch"] + - name: ingress-controller-viewer + namespaceName: warnet-logging + rules: + - apiGroups: ["networking.k8s.io"] + resources: ["ingresses"] + verbs: ["list", "get", "watch"] +podLimit: 100 diff --git a/resources/graphs/64tanks_txrate.graphml b/resources/graphs/64tanks_txrate.graphml deleted file mode 100644 index 98112d70e..000000000 --- a/resources/graphs/64tanks_txrate.graphml +++ /dev/null @@ -1,1165 +0,0 @@ - - - - - - - - - - - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - bitcoindevproject/bitcoin:27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - 27.0 - - - - True - txrate=getchaintxstats(10)["txrate"] blocks=getblockcount() mempool_size=getmempoolinfo()["size"] - False - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/graphs/default.graphml b/resources/graphs/default.graphml deleted file mode 100644 index ce84579df..000000000 --- a/resources/graphs/default.graphml +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - - - - 27.0 - -uacomment=w0 - true - true - - - 27.0 - -uacomment=w1 - true - true - - - bitcoindevproject/bitcoin:26.0 - -uacomment=w2 -debug=mempool - true - true - - - 27.0 - -uacomment=w3 - true - - - 27.0 - -uacomment=w4 - true - - - 27.0 - -uacomment=w5 - true - - - 27.0 - -uacomment=w6 - - - 27.0 - -uacomment=w7 - - - 27.0 - -uacomment=w8 - - - 27.0 - -uacomment=w9 - - - 27.0 - -uacomment=w10 - - - 27.0 - -uacomment=w11 - - - - - - - - - - - - - - - - diff --git a/resources/graphs/tx_relay/15tps-100nodes-withoutpr.graphml b/resources/graphs/tx_relay/15tps-100nodes-withoutpr.graphml deleted file mode 100644 index 02bfd30b2..000000000 --- a/resources/graphs/tx_relay/15tps-100nodes-withoutpr.graphml +++ /dev/null @@ -1,1688 +0,0 @@ - - - - - - - - - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - True - True - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - True - True - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - True - True - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - True - True - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - True - True - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - True - True - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - True - True - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - True - True - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - True - True - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - True - True - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - bitcoindevproject/tx-relay-test:without-pr - -blockmaxweight=3996000 -debug=cmpctblock - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/images/bitcoin/Dockerfile b/resources/images/bitcoin/Dockerfile index 2e4a7d1e2..6efba5335 100644 --- a/resources/images/bitcoin/Dockerfile +++ b/resources/images/bitcoin/Dockerfile @@ -43,7 +43,7 @@ RUN set -ex \ && ./autogen.sh \ && ./configure \ LDFLAGS=-L`ls -d /opt/db*`/lib/ \ - CPPFLAGS=-I`ls -d /opt/db*`/include/ \ + CPPFLAGS="-g0 -I`ls -d /opt/db*`/include/ --param ggc-min-expand=1 --param ggc-min-heapsize=32768" \ --prefix=${BITCOIN_PREFIX} \ ${BUILD_ARGS} \ && make -j$(nproc) \ diff --git a/resources/images/bitcoin/Dockerfile.dev b/resources/images/bitcoin/Dockerfile.dev new file mode 100644 index 000000000..7fd84537b --- /dev/null +++ b/resources/images/bitcoin/Dockerfile.dev @@ -0,0 +1,82 @@ +# Setup deps stage +FROM alpine:3.20 AS deps +ARG REPO +ARG COMMIT_SHA +ARG BUILD_ARGS + +RUN --mount=type=cache,target=/var/cache/apk \ + sed -i 's/http\:\/\/dl-cdn.alpinelinux.org/https\:\/\/alpine.global.ssl.fastly.net/g' /etc/apk/repositories \ + && apk --no-cache add \ + cmake \ + python3 \ + boost-dev \ + build-base \ + chrpath \ + file \ + gnupg \ + git \ + libevent-dev \ + libressl \ + libtool \ + linux-headers \ + sqlite-dev \ + zeromq-dev \ + capnproto-dev + +COPY isroutable.patch /tmp/ +COPY addrman.patch /tmp/ + + +# Clone and patch and build stage +FROM deps AS build +ENV BITCOIN_PREFIX=/opt/bitcoin +WORKDIR /build + +RUN set -ex \ + && cd /build \ + && git clone --depth 1 "https://github.com/${REPO}" \ + && cd bitcoin \ + && git fetch --depth 1 origin "$COMMIT_SHA" \ + && git checkout "$COMMIT_SHA" \ + && git apply /tmp/isroutable.patch \ + && git apply /tmp/addrman.patch \ + && sed -i s:sys/fcntl.h:fcntl.h: src/compat/compat.h \ + && cmake -B build \ + -DCMAKE_INSTALL_PREFIX=${BITCOIN_PREFIX} \ + ${BUILD_ARGS} \ + && cmake --build build -j$(nproc) \ + && cmake --install build \ + && strip ${BITCOIN_PREFIX}/bin/bitcoin-cli \ + && strip ${BITCOIN_PREFIX}/bin/bitcoind \ + && rm -f ${BITCOIN_PREFIX}/lib/libbitcoinconsensus.a \ + && rm -f ${BITCOIN_PREFIX}/lib/libbitcoinconsensus.so.0.0.0 + +# Final clean stage +FROM alpine:3.20 +ARG UID=100 +ARG GID=101 +ENV BITCOIN_DATA=/root/.bitcoin +ENV BITCOIN_PREFIX=/opt/bitcoin +ENV PATH=${BITCOIN_PREFIX}/bin:$PATH +LABEL maintainer.0="bitcoindevproject" + +RUN addgroup bitcoin --gid ${GID} --system \ + && adduser --uid ${UID} --system bitcoin --ingroup bitcoin +RUN --mount=type=cache,target=/var/cache/apk sed -i 's/http\:\/\/dl-cdn.alpinelinux.org/https\:\/\/alpine.global.ssl.fastly.net/g' /etc/apk/repositories \ + && apk --no-cache add \ + bash \ + libevent \ + libzmq \ + shadow \ + sqlite-dev \ + su-exec \ + capnproto-dev + +COPY --from=build /opt/bitcoin /usr/local +COPY entrypoint.sh / + +VOLUME ["/home/bitcoin/.bitcoin"] +EXPOSE 8332 8333 18332 18333 18443 18444 38333 38332 + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["bitcoind"] diff --git a/resources/images/bitcoin/insecure/Dockerfile b/resources/images/bitcoin/insecure/Dockerfile new file mode 100644 index 000000000..ce6699872 --- /dev/null +++ b/resources/images/bitcoin/insecure/Dockerfile @@ -0,0 +1,173 @@ +# Base stage +# ---------- +# +# We use the alpine version to get the +# correct version of glibc / gcc for building older bitcoin +# core versions. + +# Default is set here to quiet a warning from Docker, but the caller must +# be sure to ALWAYS set this correct per the version of bitcoin core they are +# trying to build +ARG ALPINE_VERSION=3.7 +FROM alpine:${ALPINE_VERSION} AS base + +# Setup deps stage +# ---------------- +# +# this installs the common dependencies for all of the old versions +# and then version specific dependencies are passed via the +# EXTRA_PACKAGES ARG +FROM base AS deps +ARG EXTRA_PACKAGES="" +RUN --mount=type=cache,target=/var/cache/apk \ + sed -i 's/http\:\/\/dl-cdn.alpinelinux.org/https\:\/\/alpine.global.ssl.fastly.net/g' /etc/apk/repositories \ + && apk --no-cache add \ + autoconf \ + automake \ + boost-dev \ + build-base \ + ccache \ + chrpath \ + file \ + gnupg \ + git \ + libevent-dev \ + libressl \ + libtool \ + linux-headers \ + zeromq-dev \ + ${EXTRA_PACKAGES} + +ENV BERKELEYDB_VERSION=db-4.8.30.NC +ENV BERKELEYDB_PREFIX=/opt/${BERKELEYDB_VERSION} + +RUN wget https://download.oracle.com/berkeley-db/${BERKELEYDB_VERSION}.tar.gz +RUN tar -xzf *.tar.gz +RUN sed s/__atomic_compare_exchange/__atomic_compare_exchange_db/g -i ${BERKELEYDB_VERSION}/dbinc/atomic.h +RUN mkdir -p ${BERKELEYDB_PREFIX} + +WORKDIR /${BERKELEYDB_VERSION}/build_unix + +ARG TARGETPLATFORM +RUN if [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ + ../dist/configure --enable-cxx --disable-shared --with-pic --prefix=${BERKELEYDB_PREFIX} --build=aarch64-unknown-linux-gnu; \ +else \ + ../dist/configure --enable-cxx --disable-shared --with-pic --prefix=${BERKELEYDB_PREFIX}; \ +fi +RUN make -j$(nproc) +RUN make install +RUN rm -rf ${BERKELEYDB_PREFIX}/docs + +# Build stage +# ----------- +# +# We can build from a git repo using the REPO and COMMIT_SHA args +# or from a local directory using FROM_SRC=true and specifying the local +# source directory. Build args are set using a default but can be changed +# on an imnage by image basis, if needed +# +# PRE_CONFIGURE_COMMANDS is used for version specific fixes needed before +# running ./autogen.sh && ./configure +# +# EXTRA_BUILD_ARGS is used for version specific build flags +FROM deps AS build +ARG FROM_SRC="false" +ARG REPO="" +ARG COMMIT_SHA="" +ARG BUILD_ARGS="--disable-tests --without-gui --disable-bench --disable-fuzz-binary --enable-suppress-external-warnings" +ARG EXTRA_BUILD_ARGS="" +ARG PRE_CONFIGURE_COMMANDS="" + +COPY --from=deps /opt /opt +ENV BITCOIN_PREFIX=/opt/bitcoin +WORKDIR /build + +# Even if not being used, --build-context bitcoin-src must be specified else +# this line will error. If building from a remote repo, use something like +# --build-context bitcoin-src="." +COPY --from=bitcoin-src . /tmp/bitcoin-source +RUN if [ "$FROM_SRC" = "true" ]; then \ + # run with --progress=plain to see these log outputs + echo "Using local files from /tmp/bitcoin-source"; \ + if [ -d "/tmp/bitcoin-source" ] && [ "$(ls -A /tmp/bitcoin-source)" ]; then \ + cp -R /tmp/bitcoin-source /build/bitcoin; \ + else \ + echo "Error: Local source directory is empty or does not exist" && exit 1; \ + fi \ + else \ + echo "Cloning from git repository"; \ + git clone --depth 1 "https://github.com/${REPO}" /build/bitcoin \ + && cd /build/bitcoin \ + && git fetch --depth 1 origin "$COMMIT_SHA" \ + && git checkout "$COMMIT_SHA"; \ + fi; + +# This is not our local ccache, but ccache in the docker cache +# this does speed up builds substantially when building from source or building +# multiple versions sequentially +ENV CCACHE_DIR=/ccache +RUN --mount=type=cache,target=/ccache \ + set -ex \ + && cd /build/bitcoin \ + && if [ -n "$PRE_CONFIGURE_COMMANDS" ]; then \ + eval ${PRE_CONFIGURE_COMMANDS}; \ + fi \ + && ./autogen.sh \ + && ./configure \ + LDFLAGS=-L`ls -d /opt/db*`/lib/ \ + CPPFLAGS="-I`ls -d /opt/db*`/include/ --param ggc-min-expand=1 --param ggc-min-heapsize=32768" \ + --prefix=${BITCOIN_PREFIX} \ + ${BUILD_ARGS} \ + ${EXTRA_BUILD_ARGS} \ + --with-daemon \ + && make -j$(nproc) \ + && make install \ + && strip ${BITCOIN_PREFIX}/bin/bitcoin-cli \ + && strip ${BITCOIN_PREFIX}/bin/bitcoind \ + && rm -f ${BITCOIN_PREFIX}/lib/libbitcoinconsensus.a \ + && rm -f ${BITCOIN_PREFIX}/lib/libbitcoinconsensus.so.0.0.0 \ + && rm -f ${BITCOIN_PREFIX}/bin/bitcoin-tx \ + && rm -f ${BITCOIN_PREFIX}/bin/bitcoin-wallet + +# verify ccache is working, specify --progress=plain to see output in build logs +RUN ccache -s + +# Final clean stage +# ----------------- +# +# EXTRA_RUNTIME_PACKAGES is used for version specific runtime deps +FROM alpine:${ALPINE_VERSION} +ARG EXTRA_RUNTIME_PACKAGES="" +ARG UID=100 +ARG GID=101 +ARG BITCOIN_VERSION +ENV BITCOIN_DATA=/root/.bitcoin +ENV BITCOIN_PREFIX=/opt/bitcoin +ENV PATH=${BITCOIN_PREFIX}/bin:$PATH +ENV BITCOIN_VERSION=${BITCOIN_VERSION} +LABEL maintainer.0="bitcoindevproject" + +RUN addgroup -g ${GID} -S bitcoin +RUN adduser -u ${UID} -S bitcoin -G bitcoin +RUN --mount=type=cache,target=/var/cache/apk sed -i 's/http\:\/\/dl-cdn.alpinelinux.org/https\:\/\/alpine.global.ssl.fastly.net/g' /etc/apk/repositories \ + && apk --no-cache add \ + bash \ + boost-filesystem \ + boost-system \ + boost-thread \ + libevent \ + libzmq \ + shadow \ + sqlite-dev \ + su-exec \ + ${EXTRA_RUNTIME_PACKAGES} + +COPY --from=build /opt/bitcoin /usr/local +COPY entrypoint.sh /entrypoint.sh + +VOLUME ["/home/bitcoin/.bitcoin"] +EXPOSE 8332 8333 18332 18333 18443 18444 38333 38332 + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["bitcoind"] + diff --git a/resources/images/bitcoin/insecure/addrman_v0.16.1.patch b/resources/images/bitcoin/insecure/addrman_v0.16.1.patch new file mode 100644 index 000000000..bc8a73bf8 --- /dev/null +++ b/resources/images/bitcoin/insecure/addrman_v0.16.1.patch @@ -0,0 +1,13 @@ +diff --git a/src/netaddress.cpp b/src/netaddress.cpp +index 4fbfa2b5c85..0d8d5751268 100644 +--- a/src/netaddress.cpp ++++ b/src/netaddress.cpp +@@ -455,6 +455,8 @@ std::vector CNetAddr::GetGroup() const + vchRet.push_back(NET_IPV4); + vchRet.push_back(GetByte(3) ^ 0xFF); + vchRet.push_back(GetByte(2) ^ 0xFF); ++ vchRet.push_back(GetByte(1) ^ 0xFF); ++ vchRet.push_back(GetByte(0) ^ 0xFF); + return vchRet; + } + else if (IsTor()) diff --git a/resources/images/bitcoin/insecure/addrman_v0.17.0.patch b/resources/images/bitcoin/insecure/addrman_v0.17.0.patch new file mode 100644 index 000000000..265a30b6e --- /dev/null +++ b/resources/images/bitcoin/insecure/addrman_v0.17.0.patch @@ -0,0 +1,13 @@ +diff --git a/src/netaddress.cpp b/src/netaddress.cpp +index 778c2700f95..03d97bcd673 100644 +--- a/src/netaddress.cpp ++++ b/src/netaddress.cpp +@@ -354,6 +354,8 @@ std::vector CNetAddr::GetGroup() const + vchRet.push_back(NET_IPV4); + vchRet.push_back(GetByte(3) ^ 0xFF); + vchRet.push_back(GetByte(2) ^ 0xFF); ++ vchRet.push_back(GetByte(1) ^ 0xFF); ++ vchRet.push_back(GetByte(0) ^ 0xFF); + return vchRet; + } + else if (IsTor()) diff --git a/resources/images/bitcoin/insecure/addrman_v0.19.2.patch b/resources/images/bitcoin/insecure/addrman_v0.19.2.patch new file mode 100644 index 000000000..bc8a73bf8 --- /dev/null +++ b/resources/images/bitcoin/insecure/addrman_v0.19.2.patch @@ -0,0 +1,13 @@ +diff --git a/src/netaddress.cpp b/src/netaddress.cpp +index 4fbfa2b5c85..0d8d5751268 100644 +--- a/src/netaddress.cpp ++++ b/src/netaddress.cpp +@@ -455,6 +455,8 @@ std::vector CNetAddr::GetGroup() const + vchRet.push_back(NET_IPV4); + vchRet.push_back(GetByte(3) ^ 0xFF); + vchRet.push_back(GetByte(2) ^ 0xFF); ++ vchRet.push_back(GetByte(1) ^ 0xFF); ++ vchRet.push_back(GetByte(0) ^ 0xFF); + return vchRet; + } + else if (IsTor()) diff --git a/resources/images/bitcoin/insecure/addrman_v0.20.0.patch b/resources/images/bitcoin/insecure/addrman_v0.20.0.patch new file mode 100644 index 000000000..db638357c --- /dev/null +++ b/resources/images/bitcoin/insecure/addrman_v0.20.0.patch @@ -0,0 +1,13 @@ +diff --git a/src/netaddress.cpp b/src/netaddress.cpp +index 228caf74a93..a6728321d1d 100644 +--- a/src/netaddress.cpp ++++ b/src/netaddress.cpp +@@ -517,6 +517,8 @@ std::vector CNetAddr::GetGroup(const std::vector &asmap) co + uint32_t ipv4 = GetLinkedIPv4(); + vchRet.push_back((ipv4 >> 24) & 0xFF); + vchRet.push_back((ipv4 >> 16) & 0xFF); ++ vchRet.push_back((ipv4 >> 8) & 0xFF); ++ vchRet.push_back(ipv4 & 0xFF); + return vchRet; + } else if (IsTor()) { + nStartByte = 6; diff --git a/resources/images/bitcoin/insecure/addrman_v0.21.1.patch b/resources/images/bitcoin/insecure/addrman_v0.21.1.patch new file mode 100644 index 000000000..c85679b16 --- /dev/null +++ b/resources/images/bitcoin/insecure/addrman_v0.21.1.patch @@ -0,0 +1,13 @@ +diff --git a/src/netaddress.cpp b/src/netaddress.cpp +index e0d4638dd6a..a84b3980f30 100644 +--- a/src/netaddress.cpp ++++ b/src/netaddress.cpp +@@ -742,6 +742,8 @@ std::vector CNetAddr::GetGroup(const std::vector &asmap) co + uint32_t ipv4 = GetLinkedIPv4(); + vchRet.push_back((ipv4 >> 24) & 0xFF); + vchRet.push_back((ipv4 >> 16) & 0xFF); ++ vchRet.push_back((ipv4 >> 8) & 0xFF); ++ vchRet.push_back(ipv4 & 0xFF); + return vchRet; + } else if (IsTor() || IsI2P() || IsCJDNS()) { + nBits = 4; diff --git a/resources/images/bitcoin/insecure/entrypoint.sh b/resources/images/bitcoin/insecure/entrypoint.sh new file mode 100755 index 000000000..c81d95aa9 --- /dev/null +++ b/resources/images/bitcoin/insecure/entrypoint.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -e + +if [ "$(echo "$1" | cut -c1)" = "-" ]; then + echo "$0: assuming arguments for bitcoind" + + set -- bitcoind "$@" +fi + +if [ "$(echo "$1" | cut -c1)" = "-" ] || [ "$1" = "bitcoind" ]; then + mkdir -p "$BITCOIN_DATA" + chmod 700 "$BITCOIN_DATA" + echo "$0: setting data directory to $BITCOIN_DATA" + set -- "$@" -datadir="$BITCOIN_DATA" +fi + +# Incorporate additional arguments for bitcoind if BITCOIN_ARGS is set. +if [ -n "$BITCOIN_ARGS" ]; then + IFS=' ' read -ra ARG_ARRAY <<< "$BITCOIN_ARGS" + set -- "$@" "${ARG_ARRAY[@]}" +fi + +# Conditionally add -printtoconsole for Bitcoin version 0.16.1 +if [ "${BITCOIN_VERSION}" == "0.16.1" ]; then + exec "$@" -printtoconsole +else + exec "$@" +fi diff --git a/resources/images/bitcoin/insecure/isroutable_v0.16.1.patch b/resources/images/bitcoin/insecure/isroutable_v0.16.1.patch new file mode 100644 index 000000000..0d9d4ad54 --- /dev/null +++ b/resources/images/bitcoin/insecure/isroutable_v0.16.1.patch @@ -0,0 +1,13 @@ +diff --git a/src/netaddress.cpp b/src/netaddress.cpp +index 81f72879f40..8aae93a6b68 100644 +--- a/src/netaddress.cpp ++++ b/src/netaddress.cpp +@@ -231,7 +231,7 @@ bool CNetAddr::IsValid() const + + bool CNetAddr::IsRoutable() const + { +- return IsValid() && !(IsRFC1918() || IsRFC2544() || IsRFC3927() || IsRFC4862() || IsRFC6598() || IsRFC5737() || (IsRFC4193() && !IsTor()) || IsRFC4843() || IsLocal() || IsInternal()); ++ return true; + } + + bool CNetAddr::IsInternal() const diff --git a/resources/images/bitcoin/insecure/isroutable_v0.17.0.patch b/resources/images/bitcoin/insecure/isroutable_v0.17.0.patch new file mode 100644 index 000000000..f8d1fef08 --- /dev/null +++ b/resources/images/bitcoin/insecure/isroutable_v0.17.0.patch @@ -0,0 +1,13 @@ +diff --git a/src/netaddress.cpp b/src/netaddress.cpp +index 778c2700f95..9655b01efba 100644 +--- a/src/netaddress.cpp ++++ b/src/netaddress.cpp +@@ -226,7 +226,7 @@ bool CNetAddr::IsValid() const + + bool CNetAddr::IsRoutable() const + { +- return IsValid() && !(IsRFC1918() || IsRFC2544() || IsRFC3927() || IsRFC4862() || IsRFC6598() || IsRFC5737() || (IsRFC4193() && !IsTor()) || IsRFC4843() || IsLocal() || IsInternal()); ++ return true; + } + + bool CNetAddr::IsInternal() const diff --git a/resources/images/bitcoin/insecure/isroutable_v0.19.2.patch b/resources/images/bitcoin/insecure/isroutable_v0.19.2.patch new file mode 100644 index 000000000..295569cd2 --- /dev/null +++ b/resources/images/bitcoin/insecure/isroutable_v0.19.2.patch @@ -0,0 +1,13 @@ +diff --git a/src/netaddress.cpp b/src/netaddress.cpp +index 228caf74a93..d1290d4de49 100644 +--- a/src/netaddress.cpp ++++ b/src/netaddress.cpp +@@ -300,7 +300,7 @@ bool CNetAddr::IsValid() const + */ + bool CNetAddr::IsRoutable() const + { +- return IsValid() && !(IsRFC1918() || IsRFC2544() || IsRFC3927() || IsRFC4862() || IsRFC6598() || IsRFC5737() || (IsRFC4193() && !IsTor()) || IsRFC4843() || IsRFC7343() || IsLocal() || IsInternal()); ++ return true; + } + + /** diff --git a/resources/images/bitcoin/insecure/isroutable_v0.20.0.patch b/resources/images/bitcoin/insecure/isroutable_v0.20.0.patch new file mode 100644 index 000000000..295569cd2 --- /dev/null +++ b/resources/images/bitcoin/insecure/isroutable_v0.20.0.patch @@ -0,0 +1,13 @@ +diff --git a/src/netaddress.cpp b/src/netaddress.cpp +index 228caf74a93..d1290d4de49 100644 +--- a/src/netaddress.cpp ++++ b/src/netaddress.cpp +@@ -300,7 +300,7 @@ bool CNetAddr::IsValid() const + */ + bool CNetAddr::IsRoutable() const + { +- return IsValid() && !(IsRFC1918() || IsRFC2544() || IsRFC3927() || IsRFC4862() || IsRFC6598() || IsRFC5737() || (IsRFC4193() && !IsTor()) || IsRFC4843() || IsRFC7343() || IsLocal() || IsInternal()); ++ return true; + } + + /** diff --git a/resources/images/bitcoin/insecure/isroutable_v0.21.1.patch b/resources/images/bitcoin/insecure/isroutable_v0.21.1.patch new file mode 100644 index 000000000..ab8cbb7a5 --- /dev/null +++ b/resources/images/bitcoin/insecure/isroutable_v0.21.1.patch @@ -0,0 +1,13 @@ +diff --git a/src/netaddress.cpp b/src/netaddress.cpp +index e0d4638dd6a..2615e076b50 100644 +--- a/src/netaddress.cpp ++++ b/src/netaddress.cpp +@@ -465,7 +465,7 @@ bool CNetAddr::IsValid() const + */ + bool CNetAddr::IsRoutable() const + { +- return IsValid() && !(IsRFC1918() || IsRFC2544() || IsRFC3927() || IsRFC4862() || IsRFC6598() || IsRFC5737() || (IsRFC4193() && !IsTor()) || IsRFC4843() || IsRFC7343() || IsLocal() || IsInternal()); ++ return true; + } + + /** diff --git a/resources/images/commander/Dockerfile b/resources/images/commander/Dockerfile new file mode 100644 index 000000000..4d526b0a9 --- /dev/null +++ b/resources/images/commander/Dockerfile @@ -0,0 +1,6 @@ +# Use an official Python runtime as the base image +FROM python:3.12-slim + +# Python dependencies +RUN pip install --no-cache-dir kubernetes +RUN pip install --no-cache-dir pyln-proto \ No newline at end of file diff --git a/resources/images/exporter/bitcoin-exporter.py b/resources/images/exporter/bitcoin-exporter.py index 8b2cbec25..e64f90c67 100644 --- a/resources/images/exporter/bitcoin-exporter.py +++ b/resources/images/exporter/bitcoin-exporter.py @@ -28,7 +28,7 @@ def auth_proxy_request(self, method, path, postdata): # label=method(params)[return object key][...] METRICS = os.environ.get( "METRICS", - 'blocks=getblockcount() inbounds=getnetworkinfo()["connections_in"] outbounds=getnetworkinfo()["connections_in"] mempool_size=getmempoolinfo()["size"]', + 'blocks=getblockcount() inbounds=getnetworkinfo()["connections_in"] outbounds=getnetworkinfo()["connections_out"] mempool_size=getmempoolinfo()["size"]', ) # Set up bitcoind RPC client @@ -45,6 +45,13 @@ def make_metric_function(cmd): return None +def make_counting_function(cmd, key, value): + try: + return lambda: sum(1 for x in eval(f"rpc.{cmd}") if x.get(key) == value) + except Exception: + return None + + # Parse RPC queries into metrics commands = METRICS.split(" ") for labeled_cmd in commands: @@ -53,7 +60,12 @@ def make_metric_function(cmd): label, cmd = labeled_cmd.strip().split("=") # label, description i.e. ("bitcoin_conn_in", "Number of connections in") metric = Gauge(label, cmd) - metric.set_function(make_metric_function(cmd)) + if "COUNT:" in cmd: + _, args = cmd.split(":") + cmd, key, value = args.split(",") + metric.set_function(make_counting_function(cmd, key, value)) + else: + metric.set_function(make_metric_function(cmd)) print(f"Metric created: {labeled_cmd}") # Start the server diff --git a/resources/images/rpc/Dockerfile_dev b/resources/images/rpc/Dockerfile_dev deleted file mode 100644 index 48f348228..000000000 --- a/resources/images/rpc/Dockerfile_dev +++ /dev/null @@ -1,29 +0,0 @@ -# Use an official Python runtime as the base image -FROM python:3.12-slim - -# Install procps, which includes pgrep -RUN apt-get update && \ - apt-get install -y procps openssh-client && \ - rm -rf /var/lib/apt/lists/* - -# Install `uv` package installer (https://github.com/astral-sh/uv) -RUN pip install uv - -# Set the working directory in the container -WORKDIR /root/warnet - -# Make port 9276 available to the world outside this container -# Change the port if your server is running on a different port -EXPOSE 9276 - -# Instead of copying the source code and installing dependencies at build time, -# we defer this to the entrypoint script for dev mode to enable hot-reloading. - -# Copy the entrypoint script into the container -COPY entrypoint.sh / - -# Set the entrypoint script to run when the container launches -ENTRYPOINT ["/entrypoint.sh"] - -# Default command -CMD ["warnet", "--dev"] diff --git a/resources/images/rpc/Dockerfile_prod b/resources/images/rpc/Dockerfile_prod deleted file mode 100644 index f34f0833f..000000000 --- a/resources/images/rpc/Dockerfile_prod +++ /dev/null @@ -1,29 +0,0 @@ -# Use an official Python runtime as the base image -FROM python:3.12-slim - -# Install procps, which includes pgrep -RUN apt-get update && \ - apt-get install -y procps openssh-client && \ - rm -rf /var/lib/apt/lists/* - -# Install `uv` package installer (https://github.com/astral-sh/uv) -RUN pip install uv - -# Set the working directory in the container -WORKDIR /root/warnet - -# Get better caching by installing before copying code -COPY requirements.txt . -RUN uv pip install --system --no-cache -r requirements.txt - -# Copy the source directory contents into the container -COPY . /root/warnet -# Install Warnet scripts -RUN uv pip install --system . - -# Make port 9276 available to the world outside this container -# Change the port if your server is running on a different port -EXPOSE 9276 - -# Run server.py when the container launches -CMD ["warnet"] diff --git a/resources/images/rpc/Dockerfile_rpc.dockerignore b/resources/images/rpc/Dockerfile_rpc.dockerignore deleted file mode 100644 index d19d3d622..000000000 --- a/resources/images/rpc/Dockerfile_rpc.dockerignore +++ /dev/null @@ -1,16 +0,0 @@ -.git - -**/__pycache__ -.trunk -.venv -build -dist -**/*.egg-info -**/*.egg/ -**/*.pyc -**/*.swp - -frontend -.ruff_cache -.idea -.ignored_extras diff --git a/resources/images/rpc/entrypoint.sh b/resources/images/rpc/entrypoint.sh deleted file mode 100755 index bb9d0e7d2..000000000 --- a/resources/images/rpc/entrypoint.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -set -e - -SOURCE_DIR="/root/warnet" -MAX_ATTEMPTS=30 -SLEEP_DURATION=1 - -echo "Checking for mounted source code at ${SOURCE_DIR}..." - -check_setup_toml() { - if [ -f "${SOURCE_DIR}/pyproject.toml" ]; then - return 0 - else - return 1 - fi -} - -attempt=1 -while ! check_setup_toml; do - echo "Waiting for source code to be mounted (attempt: ${attempt}/${MAX_ATTEMPTS})..." - sleep ${SLEEP_DURATION} - ((attempt++)) - - if [ ${attempt} -gt ${MAX_ATTEMPTS} ]; then - echo "Source code not mounted after ${MAX_ATTEMPTS} attempts. Proceeding without installation." - break - fi -done - -# If setup.py is found, install the package -if check_setup_toml; then - echo "Installing package from ${SOURCE_DIR}..." - cd ${SOURCE_DIR} - uv pip install --system --no-cache -e . -fi - -# Execute the CMD from the Dockerfile -exec "$@" diff --git a/resources/images/sidecar/Dockerfile b/resources/images/sidecar/Dockerfile deleted file mode 100644 index ec7f6a787..000000000 --- a/resources/images/sidecar/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM alpine:latest - -RUN apk add openssh - -RUN echo "root:" | chpasswd - -RUN ssh-keygen -A - -CMD ["/usr/sbin/sshd", "-D", \ - "-o", "PasswordAuthentication=yes", \ - "-o", "PermitEmptyPasswords=yes", \ - "-o", "PermitRootLogin=yes"] \ No newline at end of file diff --git a/resources/images/tor/Dockerfile_tor_da b/resources/images/tor/Dockerfile_tor_da index bbc2ecd2d..242f6f88f 100644 --- a/resources/images/tor/Dockerfile_tor_da +++ b/resources/images/tor/Dockerfile_tor_da @@ -1,18 +1,18 @@ FROM debian:bookworm-slim ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y tor +RUN apt-get update && apt-get install -y tor iproute2 dnsutils RUN mkdir -p /home/debian-tor/.tor/keys RUN chown -R debian-tor:debian-tor /home/debian-tor RUN mkdir -p /var/log/tor RUN chown -R debian-tor:debian-tor /var/log/tor -COPY tor/tor-keys /home/debian-tor/.tor/keys +COPY ./tor-keys /home/debian-tor/.tor/keys RUN chown -R debian-tor:debian-tor /home/debian-tor/.tor/keys -COPY tor/torrc.da /etc/tor/torrc +COPY ./torrc.da /etc/tor/torrc -EXPOSE 9050 +COPY ./tor-entrypoint.sh /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] -USER debian-tor -CMD ["tor", "-f", "/etc/tor/torrc"] diff --git a/resources/images/tor/Dockerfile_tor_relay b/resources/images/tor/Dockerfile_tor_relay index bf03346a4..8cb63a7f9 100644 --- a/resources/images/tor/Dockerfile_tor_relay +++ b/resources/images/tor/Dockerfile_tor_relay @@ -1,17 +1,16 @@ FROM debian:bookworm-slim ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update && apt-get install -y tor iproute2 gosu +RUN apt-get update && apt-get install -y tor iproute2 dnsutils -RUN mkdir -p /home/debian-tor/.tor/keys +RUN mkdir -p /home/debian-tor/.tor RUN chown -R debian-tor:debian-tor /home/debian-tor RUN mkdir -p /var/log/tor RUN chown -R debian-tor:debian-tor /var/log/tor -COPY tor/torrc.relay /etc/tor/torrc -EXPOSE 9050 +COPY ./torrc.relay /etc/tor/torrc -COPY tor/tor-entrypoint.sh /entrypoint.sh -ENTRYPOINT /entrypoint.sh -CMD ["tor", "-f", "/etc/tor/torrc"] +COPY ./tor-entrypoint.sh /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/resources/images/tor/README.md b/resources/images/tor/README.md new file mode 100644 index 000000000..03bb281f8 --- /dev/null +++ b/resources/images/tor/README.md @@ -0,0 +1,30 @@ +## Refresh Tor DA V3 Authority Keys + +1. Generate new authority identity key (should only need to do this once) + +From `tor/tor-keys/` directory: + +``` +tor-gencert --create-identity-key -i authority_identity_key +``` + +The PEM passphrase used is `warnet` + +2. Generate new certificates (expires in 24 months) + +``` +tor-gencert -i authority_identity_key -s authority_signing_key -c authority_certificate -m 24 +``` + +3. Configure tor + +Copy the `fingerprint` value from line 2 in `authority_certificate` and paste in to the top of `tor-entrypoint.sh` as `V3IDENT` + + +## Build and upload Warnet images + +From repository root: + +``` +./resources/images/tor/build-tor.sh +``` \ No newline at end of file diff --git a/resources/images/tor/build-tor.sh b/resources/images/tor/build-tor.sh new file mode 100755 index 000000000..5efb5447e --- /dev/null +++ b/resources/images/tor/build-tor.sh @@ -0,0 +1,15 @@ +SCRIPT_PATH="$(realpath "${BASH_SOURCE[0]}")" + +docker buildx build \ + --platform linux/amd64,linux/arm64,linux/armhf \ + --push \ + -t bitcoindevproject/tor-da:latest \ + -f ./resources/images/tor/Dockerfile_tor_da \ + ./resources/images/tor/ + +docker buildx build \ + --platform linux/amd64,linux/arm64,linux/armhf \ + --push \ + -t bitcoindevproject/tor-relay:latest \ + -f ./resources/images/tor/Dockerfile_tor_relay \ + ./resources/images/tor/ \ No newline at end of file diff --git a/resources/images/tor/tor-entrypoint.sh b/resources/images/tor/tor-entrypoint.sh index bebaef89b..6fa3d08e9 100755 --- a/resources/images/tor/tor-entrypoint.sh +++ b/resources/images/tor/tor-entrypoint.sh @@ -1,7 +1,23 @@ #!/bin/bash set -e -echo "Address $(ip addr show eth0 | grep "inet\b" | awk '{print $2}' | cut -d/ -f1)" >> /etc/tor/torrc -# mkdir -p /home/debian-tor/.tor/keys -# chown -R debian-tor:debian-tor /home/debian-tor -gosu debian-tor tor -f /etc/tor/torrc +V3IDENT=D9ED7BC69543F1477968C312E7EEEB933BC4EF6D + +echo "Starting tor-entrypoint.sh" + +IP_ADDR=$(ip addr show eth0 | grep "inet\b" | awk '{print $2}' | cut -d/ -f1) + +until TORDA_IP=$(dig +short torda.default.svc.cluster.local); do + echo "Waiting for DNS: torda" + sleep 2 +done + +echo "My IP address: $IP_ADDR" +echo "Directory Authority IP address: $TORDA_IP" + +echo "Address $IP_ADDR" >> /etc/tor/torrc +echo "DirAuthority orport=9001 no-v2 v3ident=$V3IDENT $TORDA_IP:9030 03E942A4F12D85B2CF7CBA4E910F321AE98EC233" >> /etc/tor/torrc + +cat /etc/tor/torrc + +su -s /bin/sh debian-tor -c 'tor' diff --git a/resources/images/tor/tor-keys/authority_certificate b/resources/images/tor/tor-keys/authority_certificate index 4c4bcaad9..19f3eef42 100644 --- a/resources/images/tor/tor-keys/authority_certificate +++ b/resources/images/tor/tor-keys/authority_certificate @@ -1,45 +1,45 @@ dir-key-certificate-version 3 -fingerprint 15E09A6BE3619593076D8324A2E1DBEEAD4539CD -dir-key-published 2023-09-08 18:28:10 -dir-key-expires 2025-09-08 18:28:10 +fingerprint D9ED7BC69543F1477968C312E7EEEB933BC4EF6D +dir-key-published 2025-09-09 15:29:22 +dir-key-expires 2027-09-09 15:29:22 dir-identity-key -----BEGIN RSA PUBLIC KEY----- -MIIBigKCAYEAs1eKRRP+mWy2XpLbkY3dPkEfdKIfPMDDiG3o/Xu0c3fin1aJ32uG -BY3PGtwS2KQZHEJETjSaACq+2x9+fb4RV5bWhaptbt6l7dvDn15NbEklnkV3sx86 -VxiKqybUp5IQ3cNkM0AxUmQ1M/N+zzIe95T1XplaHVBSQN4xhJgYlDkDkNexIHpP -0uFxjO+2hro3cMRotjqR6xZw5OWKVJh5Is8jiSUs57xomeUy5HaUsADw46b88Ff9 -iVfTP975jzuFF7H0kKOxRpIZGRnfnuDdAtYXWHYXtIVwKBdSeHeK/N53++kxtGET -AcyWoHqO2xlvhtPbiKLHkwq0nNFFfwnhgJJSWHqYs6YI300pEMRHJMFWNsZwgwVE -dplmxnOLv9/TnJsD06d+kc05/JTfZXE/3JFs4rMci0jIhMlw+HtFfRwpDMjGI00u -VyADI0DMYTFWr+wFWrhs5N17BdQVSZUZlfxL5trZFdQpUZhxuKJowNLV1EdWzHFJ -srwV+C0LLWr/AgMBAAE= +MIIBigKCAYEArVKyRDT0nfpLBNwJgp6BBSJXZNT9c2NuADheEFP61DY8s5tZmXFE +hMahb++e+pQqFg4qCRsD91enS+psf6NoUqeuKiLlrAuRqeZRP1sFi6srvLr0Ytbv +OTwtNxC65s3aHjWudYfO4MAL83pnOSrTj5cQ9WMKZSM7pWTT9DVzVqWcGxJ/ykhr +6NoXdqu5VIJillI9mzqKwANCPqykbXO0qX3nvr0fJiJXMMNiSXquIgSWRRJrtOp1 +AuI9Wukkx+q03TS5MZRa1MU8YgXzFmVfr4Erhik3RfS5NO8N2m2qqH4cef2HzIUQ +tlFuq6kn76j8H0R6ut3s+AYhIXe4zfN/quK5nTLnMI3JbhfXhpNHAyTsCFBCE0Am +t0gh6Vh4epukQpS4FK6Bt5tCIaeuAS4PruvaN3eNHg3RnYDrfg071G8TP5ye0l5l +hkD4j8u1mdagqykFBrxRRHYM6BVDQPSkuqshPuu9FGOcOpeB+GlC8HpmNuJwwlyg +02b2x46Eu23jAgMBAAE= -----END RSA PUBLIC KEY----- dir-signing-key -----BEGIN RSA PUBLIC KEY----- -MIIBCgKCAQEAyARJ1Nano6bZsTf3UplBhaP8BfrhvDLrJmmk8x4sAot0aHPG/eOH -qGak3y3CM1I+uxozyE58w9mBOuueUIzZelUZVjgg91dsqT5/3lIYEhB4riV29Of0 -AJeh2uibEVrv1ecXo5HYFKEcCHMRTvZaWIWSKjV6TPqPbpXhDBwIcZ+/tHpAml8h -CfGMFRYIEmisNL9xjoNU1R4Iyu07xQw85+xeMU/9UJgsXnvkqkAPbAhwxZq9/8yj -/9V4jcE5NR0KdDuPblEcD5ZNjMckUeTzuVDLgdsazOROD05zaTx8kP1UixJdon8Z -oJ/fsNWsgpzNv6ns8BIwwEAOnd7seXlmfQIDAQAB +MIIBCgKCAQEAnZVQviMvuflUfqgSLbVrH79ljdihFTWYgNefw1ewdYtODWsFvAMT +Ttd+WpIDipAx3+LiZXgSNgzyVAMKpvozZWVaKPMKyf4zNqI0qT0AakborZSlgrOY +eFbyWc+0FIU7pfuVT/ukWEBxebif5EXe+X2z4LuCoUpV/xZ2oU7LOhOAJ8Dnm6B3 +M8jwIouEMWxtIYVYVeObvI81Q8rADzERtRPDlP4gd9hwoqVTnD3u8xY3Z5cmiVCt +nJmiiQPVkXItLqZDtajVPqTF8pDySsIo/CssFdU1MWs+sBVFKM+c/Z7LpmipC7pV +pFw0B4BmKR8x51I3QSSKQ7V66IjH2WP3JwIDAQAB -----END RSA PUBLIC KEY----- dir-key-crosscert -----BEGIN ID SIGNATURE----- -DJzp0QmipBz3IJ6zWziyBEkNBPA5J6QDhq2XDFNjdq2RYcOh1hZ1D089587wP3Xg -FoQehBDQccZCrmerEegdZ89dk+QXeNWrGGVpRwp7v+ok7lRPooC1IV3BIHcLs0BQ -fm7d3kYByVl1IaY7D9mpKG6COS/WOrKEnxp75YW18KEH7oIA60c395DfEkUZvDWt -a2ba2eBizM76cFUknQNlExWkw32DNtj1Axz9QyS6IQmyGxvlA1kWL6hiIPAV16NS -E5FcU/wQHL7a7tqAM3UkuLKT3nhcbdYGcaYpbzU/4jF61bzm7ETcqqL40EMQg5Hs -y3kcXZ4ItPFBn/LISp/zCg== +Q6O+oPe4NJYj3fHXJ/nxrwI6hBeRjyYMvwYzA3fKu7ow+2jfayMHB2k1zXUpoQKq +2GphDAr55RKPEREYkNRWNcJEPWVHzVzhqZITJDeDnHZMIYU4LlUBm59xOHJfv7F2 +m8cgRTPxEzn9bg1qx5/1zMv4tSwIX40VEBYijH2nWzl6pgds/B8bRnOoTCDQ5ieP +VpxKm4qpBWA8ePsiQkVOegKKVlEFkPmQZDF7HV5EQ5cKfumWvdf5mSgXQoqhn9MQ +4DM7YSg438tx4Cqlka+dBnX9GHm7EtiFkEsNRdlOt4tAjkIUZh1vux8Qn58GZXEl +1ikQimYdlOJNLHhaS7Oxsw== -----END ID SIGNATURE----- dir-key-certification -----BEGIN SIGNATURE----- -e9OQlThM7Y1jBIvZHYsm00ZcCR4L0JRGnzPtZhPtrczAi7dT7gdLhJZSYY5BM1VU -EB24flAJXeP03BtnOuFOURaazvW7J154sVHGQf96OyuCjSOjTlDNQLc6kNL1f0WD -bIXoJ0mPfRJzGX5NmGUa3KmW7/PgNO49VJSCuQQpBmC2qzQBPTCkATehJxCfiB8p -B6toi/ODKmXtFce1J0K9TRmAEIhmA5jzxqd3JISgGx0iP1iXmnMPHESXMS8QJ13e -U4t46NbobZDCAk7xkF/DNjuu0ZSV/IFg7EbB7f/qdIGvvK9bu7Esq5FoVr8sFZ50 -jxeNBEJgNk7Z/DwmNyBODqUfWVSnOydaqRCAxyHIbHGJGHLXKOOdym4sqOBe1O4+ -lyXjp/iIcrTN8rCoKFI9uT4DZj9rzzRv2tzQYl7Iqlt1fImaPN4PAnFpG/LOMrxe -v8S85DhkLpzl5uhojmVKAp810T9ege5MqXXGMDsisUtCyXDtqeQE6xqBDvydTm1m +K2qbsfUFzUih4a5lPR4kU77F81jHQWs11NI18pwyQ/Q71eGHCfMvaHyQdGy0hJJU +I6CcMQD3AuUIQ5AFuyIgPhlsgiyHOxfq8ZiAGbrA+/5ZHB5iwn7J5rxNnVq8tO/e +P0xHKtjiDIm0bZuqw9NINclD9C7/B0fafEnZ0NsIjNQ7omMvVKhbcQgojb9d1n9E +v59LgoylFyr7V6FJFrpZwPV7nMznEdX9L4U/ZAq85M7owEUSfDQH6Ep/aJ7PH2YE +BiMSMuyLf0qqylgYUGFI0zg+GrR93pN9ajKvomAvu5BiqoEzoQNwLe24/CIGFiN6 +S9zFDki5SoZGC9w+RIK4P8YZxrxbmPGtH0UKjlI76RXoCCb0dif2snxGd+YZHUev +8wxn2jvgUZNAfT0bGKAZQZLmys2TCnKoBALR3nf/tR5stpMFM/207XJd8OykRsve +fzHmCLp0xO7Son6xG9urJ4qAOuBYcxdbAOtASNIsMtxTMhA+HqECZ+h8X5BZM9Q4 -----END SIGNATURE----- diff --git a/resources/images/tor/tor-keys/authority_identity_key b/resources/images/tor/tor-keys/authority_identity_key index e3b484bae..398beab1e 100644 --- a/resources/images/tor/tor-keys/authority_identity_key +++ b/resources/images/tor/tor-keys/authority_identity_key @@ -1,41 +1,41 @@ -----BEGIN ENCRYPTED PRIVATE KEY----- -MIIHKjAcBgoqhkiG9w0BDAEDMA4ECBw6S2JG/GCKAgIIAASCBwgnCUGiqRPi2GZW -UBCZMd3jnzhts341Ry1AfHZgpn+kKa1gDbkTbW6aqEsmQJS/janksZnw2W+446fE -01JOo1tQ0OBdWbd++lbDv3sQLVHX4xDVB7HDnUG6ddrnTq2Dc2YS99A6E6Ss9mgp -Ua4XLFyB6gXp3QOdO5QT6Q50DGmGind2xmjqDO+fhupv/dXHh/DWgwhxFMHmqb+y -a7HTZuHOVsMDc/a6ZrGIq85l3NQIWm8+kXcNTHZidG3n6ydakZIvV6Jzh+1R1rEa -9TO9OZDUTVj2PHO95WCVJLWB7JmQINl1VZkEtcvz+LcyIegW3zr9b10026eWe0sj -ymK+CE9hh9Ia2JJA7KZOKqvgzZrWSKPF4Bu1BIRlnJ28iKmUzh25Fq28P+T0m+r/ -qvyuDxmmaDOUetUYXT8YRNd3Jfh6Fb+JNu6E35tyBloNDwQNB/WHgZiGpJ+TRSJr -7I+9v2pWZovI/7TwcZwxrLeHBRX56SD4wp26Ac7yD9RZk138EBPzHtuRww4Yo8XY -FJ0HFX/kGoFJo0GaKaWNMxcFEnefI3KxJB/fYcawTnkHFB/u8LqtjJ7n40zMsyib -jQhBQqmJ5asEarOQRrBwjRi59CvA7GHJEsl+WFTrMcpaL/UpPrAFYRtIHKVHorcI -iqtt8vVASE8Y9dfArA67FwPEhemwvVv1yPGBMewxvJkkMoNHU2NMd7lT8tQEGs9B -kTamf33NZcRjfoBw2apK2vxP8WqiardgzFZW1zNvKQCbsCcQodtjKNegh0AcVOZr -7rs2dX8dK02OPJ66/MkButMvOzxjf9Lou7nUDxo4zBE4LkWmy3gNtZwXoAHfWd60 -GNGKLyw9cK5hjEMfJobt3u/i2pRKsHxc/ZKv+aOCp8U6q/jdLW2uUYuw2bL0LVUD -K4Yu4iEpWYgQA1kXJHOxh4+3iZlmEQhF+PDD1w+M+5kysuOn0ZjG4jEwa1umVyjq -DPw59qrzG7U5ud6ZWjAys5OMM54tYFTbiRtwNkTIFZu7/gUHoPpiBSfD5qGxMcUw -ZC2NoLB6Z0ijiQLJcU52xmFlcipV6GCYAPcJOumGw+czPurSM5UvMuWb8G+UIer6 -T/iIyXYrhOWtYfGOs7pzNWx3USaZYQblnx+gHmD5LxR7YXRmwqLfadlsRATsw6Qj -q1/0hDWB+j3Ckf+alBzbSmDsX2/b43kskAZRPBpxecG4VW3HwQbDIckzFtM6NWFv -cYP2ENzeNR3Qy+8E87l0ZZPLXWXykH6Dsas94oKesTHN86LSpznODZMIMMjHntzo -IGR82pi6O+01ntcWXeJBhamy4eG0fl9Klfu6wJ3r67pe/9jHlWzl8JaulZ15+Myx -c3e/qtG3emT07diOXo+9ChalIYwvmiL87DlhoqREu+MsoYJ5NgnA6aQQA0BoeIEi -ZRjTzPjx+vNsk2leEoWEb34e+ft7ebgZEdak1zaINJpFeayBMYyFoCtdegb9wBzy -tZMgCjNDCb2hbpcvyXx+0HXPDW4iP7SP92lfaDSP2tgvaxeI+mjDfX2xGTaaAzzJ -wOV6FmwlurwCOZt8uGHJnBNoTWaMumf5oXKmFP4LYskDhto66lEgr0mbj1X3f6NR -8zRKtKxaFKDKQL8ddotamei+TxVajm+lyp349AocCQD8It7h497xi5C1NZPIFdWU -bKAKTuwf+ZWX2vt/Dli0YpObmD+hG+SKU39t9vpznJC6BmWneNkZ7BRabMdDlJPO -h6HSSkcSqkqwsCbjiu73n1Wj0tLUgdqvdEwjZdmGjCm6+tffcrYZZEUVzR8ErcCY -EGq/4tOWuhZc6UdsUh8JBMJAX35xXFDHbJBEMKaKD8flZBcnV+bgDTnTKEAJAcvD -WJgtJWRr0QN+BUXumqh8exzGJJVFbb47qJiCUSu3wm4XKWv15gZgBH0ZjpI/qnHu -QtijZz7pvNx/d6jilo4ph1esIwmmSHUfXF1IFshs6BfRnajpp0d5+44p4k6U2GhB -mv2nhp5iXvCn0v79GV1iO3MNzLYCuOwm0q+YqoFiYnS67BriWqfQdgupnE0iLINE -fD6jAhIgOIQ9GQ7SdmWGtAXNFm1INDxgxTgTbdTQcBkVoTVIefqp5Prgt76qmIrg -03MZqayUA9WIItHaKMXgvoDxnUlI4wWVQ44LQnBBxsIw+Wi6GXlqpjIYQ34ICjEi -xB7+ux/Wv12heEk+VeugvB2+ZKLQoq+dtKyNsgfc+emIPWBfufDS7bg+0g8evCYt -u6e99Mm6RJp9BGaEwAPiQfd03FbAnLJmH2I0U5P5R8h7ec0H01e/flG2wqD+/ejh -keYCQIG3obSCKj7ps0GUY496aaL1OZzqDepPzliBf59sXB3myQw7IYUxGwKenrpD -AO5X54JZORhV3fvT0QE= +MIIHKjAcBgoqhkiG9w0BDAEDMA4ECAgy6dLlTElfAgIIAASCBwjrd1tHgj2gbQFl +Uosh5DKMJm/532rUSydviZZCbNrINATBuvCe3ahxwbuoUn3AhuXeAfk+BYNWgT4w +iTYpmEdbgnyeTP/F+q/ILJIXNE8kNoUoOBwpH9J7ldcQTpDi8T5E48necUbHb3/N +e3aqaU1s9CDE/Fp7YlWfGBDb1HIiKU8Ym1DHD/71+Y25KaHYPvieebU2jywNC+wo +BY6zmybtaxTqdF8bWYlHVSMbVZuoisoNvS6HRco1aZe0HMkYEHkHknRn4WYnF67O +UnbBb6dsw/rjp/FHrecJ92cGY3CaiEkyuth3YCo9vVSQ9AM3mPZsbMk2UPA9lhxD +BX+MfL2mO7ugsKkN7G1LNj/DJyBJUqWcNT97zUccqBGkvit26GsFKPF7ySmnQHt5 +MA65vnrfbZc/FQ8svW3mtUbGbZlcZhFGndthZQHIGZFdlgn9Ce9B1okDazrNkOQB +J4qXwLKR1PolmQOGrDMIGxEtJyQ9jeyuaMQxXBO1brawBRxh3qesUgMtR81g+pEu +ffCWy0pqy9H5cmX6y97RbY1pVArWW692MWndVhoa1f3Sucs/gmt62QpGdmJl2fER +sDls1exsCkR7LytduSC87S4RCSqbACKOcNiVTf29ajsVwu7b477ET3O97BjhxPN3 +I0/B94MO3dLRedF01cEk5dyEacRWNN+Im4/aiFdm6xLpai8hhTm/wKA8889stifH +dL811okTxWvUVZVAVEFG8J4IVDOoXerwtLlmWENd7Qn/JXWrSjmqH6xNA6S3luJ5 +J3MS20KWyXH1IQCW/aDYUqIpp7Pnx44Kuq3yGBo4vLtS/qBfWIz6op+4+0RA0iOI +pKYcJGcX6Zn6y2ahdc5ngHAlQHVNTMjUYrxA/ar2HUyKM4FVze6UulD9HgBHBkov +kPA1O7isOa+f76gSRMWjLXC7bItvwNnF348LJizfo0z/bcRXemv0dVp86IwCMcHq +Ulx8+nAwZ3eWcgOVb84tXwMXZ59eSrUQBkamWqrxj6bWNuZSX+Muah7hc9bgrh9+ +VLA6Kzu/zXymC5nL2WXRK5rlRJbFxANzk7oYke3NeuYTvljHw18C6XuUE4+b5VL9 +ILFAJwCEHRFDi5wQfBNK1i+VWf7nRUO90a0YDjggSJvlR70uQC0qPNNs3Sn+tDiz +8KKvaeZc8MeCoGe4IOIc4Wm186q2Y0oD5Meu00kZKhcs9bwukceC71of+BXehB2c +mcLlbGHVA927C+WVl/3ZoEn8bVUqpyRZaaaJfuil/rtQ7EYEaoAEwt78uD3+JCNU +XiESX0LpLErjuc+gtvuxPq3YB8hwpmtH+Q44TCkMqh+kPVtfxXlCTFiXSMRyllWe +eFq9id99SP/MgLdM2h0jOKdVdsF6JgDUsSzi4/Jl4WmunKBjQxXDGpagzi4NmJX2 +zM7YuJUa0Tfs6AmArnF0ImXn5pLuZQxaUJPCZF/WTuzq3UvqJ+pA+Qs3k8mouW0R +hOdDlBk7WXkAJRLIWuZ2njiVJ2hLMMOaq+H9AlRCVMjqe0PLldj6xrmBxDmn8Ea6 +nMJ8/yteFs+XiYH8BpCTeY6VobzFFAu3MtCCXdqR6HTLbuisb5Qw/ht4T6GRr4yZ +jBNo5lnUFDAE4EAg9Jvt1Ftwsvul6FDVu0iAejzmopgldxDvrmJGKAdJLaJtDUPC ++ikn1iWXAdzvVtOw7XHQPidyKyKYMDIsCbsAY5ZBoT3JNUkQEeZFLdbpxh4eDWi/ +8C4pfx8vQ9G/lUfo9uRUURcEMRTr2xv2i6yFLF7bJIfAD/zMmV//NO2J21vvG0gj +/XM9dzouAIYRYjTGpiry1Mu2Af6BdSDvWhM34DgmLsN7+br2ErIYPud3jZoNHF2g +nnHuokK9HQIian+sy7Hc6av22VI4nKCV3BD3ZI0qwgX9v50ugIA1zJMEN1rTRdUp +wmBwNJ8H70g7T4sWbhKMHztHiHwNZkJWUJihzV2swBmyeBZHRyItQcd1zZ1aW87X +NK7erJaKXCAcFNKgE30GWJMyrqJO7mwQdiP/jhoYjUYk18hEw8JJvTJYfl5cjadn +j9tIjfWhRE/+lB6rnOD5/4v29y9B0WF2qZfi8puvcKX0ydIUbI8wWEGAu3goXm2H +Yg3M17uRw/XHBN14JTnsZ9+70JTKfr/r0woKq3B8xNCivMYiVb3N/uXMpwGJNW4f +moIbReLTA4BxwrMWjtQY8rJ2gAvx+htU6Aq/lr6svbkT1hsM7ZahSWKi3zs+jhwk +St8w84loC4y6snDCstuD2gQhTCLJ2YqBH0C4dRSsnQhuILVI/SAPbcD88dKt1PIs +WQnYnUxDhKpcV0lBcvfJuIHZbG1M/qFYuKE4/+ZSvtQniJ8nqbFGDrWfIRbqXWBr +xhYqQ4VR8ylP25os7jI= -----END ENCRYPTED PRIVATE KEY----- diff --git a/resources/images/tor/tor-keys/authority_signing_key b/resources/images/tor/tor-keys/authority_signing_key index 7c4723395..dde8bc06b 100644 --- a/resources/images/tor/tor-keys/authority_signing_key +++ b/resources/images/tor/tor-keys/authority_signing_key @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAyARJ1Nano6bZsTf3UplBhaP8BfrhvDLrJmmk8x4sAot0aHPG -/eOHqGak3y3CM1I+uxozyE58w9mBOuueUIzZelUZVjgg91dsqT5/3lIYEhB4riV2 -9Of0AJeh2uibEVrv1ecXo5HYFKEcCHMRTvZaWIWSKjV6TPqPbpXhDBwIcZ+/tHpA -ml8hCfGMFRYIEmisNL9xjoNU1R4Iyu07xQw85+xeMU/9UJgsXnvkqkAPbAhwxZq9 -/8yj/9V4jcE5NR0KdDuPblEcD5ZNjMckUeTzuVDLgdsazOROD05zaTx8kP1UixJd -on8ZoJ/fsNWsgpzNv6ns8BIwwEAOnd7seXlmfQIDAQABAoIBAAm+YGdgfpb5EOfy -cUICP8AgzS1Fu7s/4sHYCdD4cmM8WRMhOBDUWvPamOOwtmIVeq4Bgy4Z7reEEBN8 -o2rKoGnhHTnHRF8wOyr30GGrmksU/NVaSLQlBuIEK6kURZY/7xOP7VBKpvNYUYXd -hHrA2Fqxb72j0HL8DhfbGspiJOIotHMHVKcPN/qb7pEPg9UOlapE8HjZf+G82l0f -CPo3tLSOVCQdn4y4DSFbC/KlwVmvcJYEdBfs/XdjbGN0ytEcLihFgoPKnLDqnl3U -jy5VVL0VO1yY9r7Vq9UmKPfWnwntAE3P5FZv/ZnYSIMNG0JYGKVzSOdco8X3qyTX -69I87LECgYEA/0zcKCzRDOu8gUPAQjttSKeFXoR0uJMonMwiGWm8mhsuB8B+Tzqn -vL3zPSdfjnhhQVmLhhVLOJddKcg8gWECmu3UB/hX9hm/J8sZ355/pdEs+zOkw6WB -MHWXZ+JsqCflGMQKB1GCvnA00IfQnHBC2K6oVkMhSjh6eWU8IrYpU20CgYEAyJCj -C7UOxGFbS/7814j3w0B3niHCzYa0td5aYo8AvT8t6fC6Suba9wqt0qfcGbDGXMpq -O9yL5+SbQohm/nd7brGQfOKzpgqjHhpXctRgvBGqSHpJ0yhKDQJ8L5cfW3JHiArh -fQ2YvgSc03Y3RIRmj4OVfV1647cbLdWAPCDsZVECgYEAhaSzfuhvCseAp15TD5jS -TX08SM0n2NNYKDSICSubykQ+JVq0BD+dPSVmZnXtBMSpjK8WZbtR5C8AWvXyDnw9 -A+NJ4l4zlaXGtksQoUn0YlYMqPdQ4gYKidaUypHx9VjlCcDdyxT1T0GntB3Uq3/s -zkcn4fhEPfkwy8md4EHhgkUCgYARxwg8tGq/q2V9QffFXwWfD+rKYHG05/jCmhfm -3ogRPjVipAzPMNE9znuDzY8r08hxVxu9fJoGDvRYHGEMsyiEskZ9W1bTI+Q7edhA -fGSqpuIyFGzQw6R0rMC3Myz7XRDMFTLRc9ATH7OK5tKVRysUE3S/rPaEkqldEayR -J9XsUQKBgQCBidpMXIxmL0DLaOX45h7l9QWK2g0l6zWhmE1XjJ+fBbkyE1jLcnaN -fPvWJEATyKn/7hbwe+ay+bp04U+jwK37XoRmHCCA+WdNlfkO8qGY+RDDVH62WKS9 -MC5pE3n4didM8kSm1OCDBHWEL3tUPoLs09zHauaqnGFy8KYAN7Y5JA== +MIIEpAIBAAKCAQEAnZVQviMvuflUfqgSLbVrH79ljdihFTWYgNefw1ewdYtODWsF +vAMTTtd+WpIDipAx3+LiZXgSNgzyVAMKpvozZWVaKPMKyf4zNqI0qT0AakborZSl +grOYeFbyWc+0FIU7pfuVT/ukWEBxebif5EXe+X2z4LuCoUpV/xZ2oU7LOhOAJ8Dn +m6B3M8jwIouEMWxtIYVYVeObvI81Q8rADzERtRPDlP4gd9hwoqVTnD3u8xY3Z5cm +iVCtnJmiiQPVkXItLqZDtajVPqTF8pDySsIo/CssFdU1MWs+sBVFKM+c/Z7Lpmip +C7pVpFw0B4BmKR8x51I3QSSKQ7V66IjH2WP3JwIDAQABAoIBAEIhelwAyjOipcnt +YIucGdOd79FiOt96uYBAfQRuVVhO2Lea1TI8nCq3FoiNxDyvHK8XOOlaRVI3hq6p +BBxk3ZLMwNR0gm+YZQeldLIdLoJrNZQ60GaUVgUSf+we1TTTYN/cP6HsOeDrUnPB +fiA/Dj7neMh0CHMFjidZ1vkE5xZ8lZGt8bFYrWVNNSIQMrCRKttNuN6OePEUn2zu +YDfz/8L1kNKsN5eCde88/usBKxEcpyGrlrDazBKLsAtekY3iLkPsTY3NlRR7vnrW +trSywdbJmBMjtSdJ4azhy+KE0tlwnoOG7jbes4rTLSB8Qw54+ksnDsJimqhZgqGY +Kp9jgHECgYEA1uPNR5uH6K6i+wB+UGo1reGCWpC0NwXHCxw2DJDgtx5z//Nb01oF +a/laiNi5WIflNC8mkmCwUn1b94ySnWYqgGzxZ6OuaA1Nt0aiCDaRLa2pZnKX+DVY +NdYZALDOJms0WJiR6A718CJIRFVpHtbEHKPE9jeBvM1gPSacDMM92V8CgYEAu7ru +mF1z1YgX45WzijSyX3vzRxP77wlBaC2wVd6mgxaUQu0oyok6zJLZ3XHujM9noU5J +1c3u6rInCNRf5dW9wXT0w7NhBW9chYCclrPd952X9ri7zCIzLPp096o3WB0LwkFO +85G+Y5OSA7+X5H20A7xNWt0JnoNCpNWMTBBeDzkCgYEAnl42jH6ANCh4LrnXXZZY +xzP6KJqaZi6Y2YRKBQrEwc/st4X3d5TRiBi180KdnjRQ5BuXtqsRZ0nB/HeaGewQ +iKNBahsETuT3EIo6ogjtB5gDz1/GxgMyZkLKHGsxErlRU7XlWfuknvv0/H9hwHuQ +/nZnkvmnYKiPM6H/wZwmOcsCgYBbBDB1kql8V3UtlADt+sqYXKIATgIRTG13st8R +YtdcKvBK2myydYIaeQBDyt8bSds2a5yEmklBZIpkT4MHDW/ogWnCEA/z8J/s4gQJ +VL9DNBbfOYVjXRlurydShCY9BCgVUFCU6o5h/MQIKTXdQ8ypGvxuF0h4n68J97cD +kdPokQKBgQC7pi2UVCBxWYkktl1EvZb9d74V9Y4x0itRcVohpJtLJEucT7qhMPUX +tfdwSZvSR5+kC15Md5TmNU5veDWOstXOC+nZrbCfRXPYs4eWsFuyUjJ8bweJ0n/E +1/u+6//TgatXFLq5lfec+CS4231UWA+K081jgIoMOoMdj+ncjhDYdA== -----END RSA PRIVATE KEY----- diff --git a/resources/images/tor/tor-keys/ed25519_master_id_public_key b/resources/images/tor/tor-keys/ed25519_master_id_public_key deleted file mode 100644 index b05aa494f..000000000 Binary files a/resources/images/tor/tor-keys/ed25519_master_id_public_key and /dev/null differ diff --git a/resources/images/tor/tor-keys/ed25519_master_id_secret_key b/resources/images/tor/tor-keys/ed25519_master_id_secret_key deleted file mode 100644 index ccc1f8039..000000000 Binary files a/resources/images/tor/tor-keys/ed25519_master_id_secret_key and /dev/null differ diff --git a/resources/images/tor/tor-keys/ed25519_signing_cert b/resources/images/tor/tor-keys/ed25519_signing_cert deleted file mode 100644 index b3736275e..000000000 Binary files a/resources/images/tor/tor-keys/ed25519_signing_cert and /dev/null differ diff --git a/resources/images/tor/tor-keys/ed25519_signing_secret_key b/resources/images/tor/tor-keys/ed25519_signing_secret_key deleted file mode 100644 index c65350151..000000000 Binary files a/resources/images/tor/tor-keys/ed25519_signing_secret_key and /dev/null differ diff --git a/resources/images/tor/tor-keys/secret_onion_key b/resources/images/tor/tor-keys/secret_onion_key deleted file mode 100644 index ebb57748b..000000000 --- a/resources/images/tor/tor-keys/secret_onion_key +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICWwIBAAKBgQCx44LveS/8Z72FSGfKK9gs+wDTKjYRcIuL87Vn7x7v1x9ilosj -wL5q7yNeHxKPW5V27Ax/rjMvsbZ92THsxMIQvYdpU8QlCO9QWJC8y0WS6nYZ42MP -JwUkV9ZF0Pf4HM7PNlW0VXps9c+szyCu9qK89QpLHtIheY1blxFckRrTnwIDAQAB -AoGAfDzENpHx5JtjbpGaA0XJzehjtBcX+egbXdwQhw0nEySwQ7+WX5r+olpv3g1f -fgXdhlfnhsjX8Ohx5sTpLE5ipfnGaF9y05V2GWaWv7TA1qzI2PNNrEKVz92lWF9d -Tx+m+mO5pCO67WxUdZ+RqCKZTie830SxAbTWQx25/W+6DQECQQDiySS7a4SIZYgP -qRerdftDCwxKhdQEoiyzEVVpnp32i2ZYuPr+YNvp9ImCajfSYQnrmAPH0yIrXhAS -UepAxwSBAkEAyM3cTsSzlYIknHbgRg8N8ZiVBWTgWKfZVn5D2pLOkyjezXhv3LNx -KKkynQFTF8mIqHT8VW8f76rzxlCQLT1IHwJAQJedmKv04YxZhmxYy4Mc/2lkJM2d -J3yxUoc7VovQ3emySs7U3iLkP+xgRf7Oy2LMGof/e6iM8OEnnrAqEi0dAQJAC6Sk -GY0ePJUHOmtKJcXJsTB/s4hd2cYhu/omRQ4uHCpKgO9yzQE6lnj5DlF9V+u/mMTv -vKRs3aCz8dPKCFV9UQJATrpPkJorFlp323nxn3bCIXu/0n2iFDhk/GZoZYIe7Zqt -12Puw3Gi6PlVRd67Cf9IVdXYBeQurOgj5ZGM8jpxOw== ------END RSA PRIVATE KEY----- diff --git a/resources/images/tor/tor-keys/secret_onion_key_ntor b/resources/images/tor/tor-keys/secret_onion_key_ntor deleted file mode 100644 index d1ed0e48d..000000000 Binary files a/resources/images/tor/tor-keys/secret_onion_key_ntor and /dev/null differ diff --git a/resources/images/tor/torrc b/resources/images/tor/torrc deleted file mode 100644 index e1b3bf675..000000000 --- a/resources/images/tor/torrc +++ /dev/null @@ -1,34 +0,0 @@ -# Common -Log err file /var/log/tor/debug.log -DataDirectory /home/debian-tor/.tor/ -RunAsDaemon 1 -ControlPort 9051 -CookieAuthentication 1 -CookieAuthFileGroupReadable 1 -DataDirectoryGroupReadable 1 -TestingTorNetwork 1 -ClientUseIPv6 0 -ClientUseIPv4 1 - -# Relay -DirAuthority orport=9001 no-v2 v3ident=15E09A6BE3619593076D8324A2E1DBEEAD4539CD 100.20.15.18:9030 03E942A4F12D85B2CF7CBA4E910F321AE98EC233 -AssumeReachable 1 -ExitRelay 0 - -# Reduce resource usage -CircuitPadding 0 -MaxMemInQueues 10 Mbytes -BridgeRecordUsageByCountry 0 -DirReqStatistics 0 -ExtraInfoStatistics 0 -HiddenServiceStatistics 0 -OverloadStatistics 0 -PaddingStatistics 0 -# BandwidthBurst 10 Mbytes -# BandwidthRate 10 Mbytes -ConstrainedSockets 1 -ConstrainedSockSize 8192 Bytes -NumEntryGuards 1 -NumDirectoryGuards 1 - -# `Address ` will be added by docker_entrypoint.sh diff --git a/resources/images/tor/torrc.da b/resources/images/tor/torrc.da index a5ef0c893..de60776e9 100644 --- a/resources/images/tor/torrc.da +++ b/resources/images/tor/torrc.da @@ -1,11 +1,9 @@ # Common -Log err stdout +Log info stdout DataDirectory /home/debian-tor/.tor RunAsDaemon 0 ControlPort 9051 ORPort 9001 IPv4Only -CookieAuthentication 1 -CookieAuthFileGroupReadable 1 DataDirectoryGroupReadable 1 ExitPolicy accept *:* @@ -14,7 +12,6 @@ ClientUseIPv6 0 ClientUseIPv4 1 # Relay -DirAuthority orport=9001 no-v2 v3ident=15E09A6BE3619593076D8324A2E1DBEEAD4539CD 100.20.15.18:9030 03E942A4F12D85B2CF7CBA4E910F321AE98EC233 AssumeReachable 1 # Directory Authority @@ -38,4 +35,6 @@ PaddingStatistics 0 ConstrainedSockets 1 ConstrainedSockSize 8192 Bytes -Address 100.20.15.18 +# `Address ` will be added by tor-entrypoint. +# `DirAuthority v3ident=... ` will be added by tor-entrypoint. + diff --git a/resources/images/tor/torrc.relay b/resources/images/tor/torrc.relay index 712f51ef3..f3131e99f 100644 --- a/resources/images/tor/torrc.relay +++ b/resources/images/tor/torrc.relay @@ -1,19 +1,19 @@ # Common -Log err stdout +Log info stdout DataDirectory /home/debian-tor/.tor RunAsDaemon 0 -ControlPort 9051 -CookieAuthentication 1 -CookieAuthFileGroupReadable 1 DataDirectoryGroupReadable 1 -ORPort 9001 + +# Bitcoin +SocksPort 9050 +ControlPort 9051 + +# Relay +ORPort 9001 IPv4Only ExitPolicy accept *:* TestingTorNetwork 1 ClientUseIPv6 0 ClientUseIPv4 1 - -# Relay -DirAuthority orport=9001 no-v2 v3ident=15E09A6BE3619593076D8324A2E1DBEEAD4539CD 100.20.15.18:9030 03E942A4F12D85B2CF7CBA4E910F321AE98EC233 AssumeReachable 1 PathsNeededToBuildCircuits 0.25 TestingDirAuthVoteExit * @@ -30,8 +30,7 @@ OverloadStatistics 0 PaddingStatistics 0 ConstrainedSockets 1 ConstrainedSockSize 8192 Bytes -# NumEntryGuards 1 -# NumDirectoryGuards 1 -# UseMicrodescriptors 1 -# `Address ` will be added by tor-entrypoint.sh +# `Address ` will be added by tor-entrypoint. +# `DirAuthority v3ident=... ` will be added by tor-entrypoint. + diff --git a/resources/manifests/grafana_values.yaml b/resources/manifests/grafana_values.yaml index 110622911..1155e12d9 100644 --- a/resources/manifests/grafana_values.yaml +++ b/resources/manifests/grafana_values.yaml @@ -12,10 +12,32 @@ datasources: - name: Loki type: loki url: http://loki-gateway.warnet-logging:80 +dashboardProviders: + dashboardproviders.yaml: + apiVersion: 1 + providers: + - name: 'default' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /var/lib/grafana/dashboards/default +extraVolumeMounts: + - name: grafana-dashboards-volume + mountPath: /var/lib/grafana/dashboards/default +extraVolumes: + - name: grafana-dashboards-volume + configMap: + name: grafana-dashboards-config grafana.ini: auth: disable_login_form: true disable_signout_menu: true + server: + # this is required to use Grafana behind a reverse proxy (caddy) + root_url: "%(protocol)s://%(domain)s:%(http_port)s/grafana/" auth.anonymous: enabled: true org_name: Main Org. diff --git a/resources/manifests/warnet-rpc-service.yaml b/resources/manifests/warnet-rpc-service.yaml deleted file mode 100644 index 4dc390959..000000000 --- a/resources/manifests/warnet-rpc-service.yaml +++ /dev/null @@ -1,21 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - namespace: warnet - annotations: - kompose.cmd: kompose convert --controller statefulset - kompose.version: 1.31.2 (a92241f79) - creationTimestamp: null - labels: - io.kompose.service: rpc - name: rpc -spec: - clusterIP: None - ports: - - name: headless - port: 9276 - targetPort: 0 - selector: - io.kompose.service: rpc -status: - loadBalancer: {} diff --git a/resources/manifests/warnet-rpc-statefulset-dev.yaml b/resources/manifests/warnet-rpc-statefulset-dev.yaml deleted file mode 100644 index 91692e35a..000000000 --- a/resources/manifests/warnet-rpc-statefulset-dev.yaml +++ /dev/null @@ -1,57 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: rpc - namespace: warnet -spec: - serviceName: "rpc" - replicas: 1 - selector: - matchLabels: - io.kompose.service: rpc - template: - metadata: - labels: - io.kompose.service: rpc - spec: - containers: - - name: warnet-rpc - imagePullPolicy: Never - image: warnet/dev - ports: - - containerPort: 9276 - volumeMounts: - - name: source-code - mountPath: /root/warnet - livenessProbe: - # fail (restart) the pod if we can't find pid of warnet - exec: - command: - - /bin/sh - - -c - - | - if pgrep -f warnet > /dev/null; then - exit 0 - else - exit 1 - fi - initialDelaySeconds: 20 - periodSeconds: 5 - failureThreshold: 3 - readinessProbe: - # mark the pod as ready if we can get a 200 response from - # the /-/healthy endpoint on port 9276. - # If we can't, don't send traffic to the pod - httpGet: - path: /-/healthy - port: 9276 - initialDelaySeconds: 1 - periodSeconds: 2 - failureThreshold: 2 - timeoutSeconds: 2 - volumes: - - name: source-code - hostPath: - path: /mnt/src - type: Directory - diff --git a/resources/manifests/warnet-rpc-statefulset.yaml b/resources/manifests/warnet-rpc-statefulset.yaml deleted file mode 100644 index 3d899ab62..000000000 --- a/resources/manifests/warnet-rpc-statefulset.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: rpc - namespace: warnet -spec: - serviceName: "rpc" - replicas: 1 - selector: - matchLabels: - io.kompose.service: rpc - template: - metadata: - labels: - io.kompose.service: rpc - spec: - containers: - - name: warnet-rpc - imagePullPolicy: Always - image: bitcoindevproject/warnet-rpc:latest - ports: - - containerPort: 9276 - diff --git a/src/test_framework/__init__.py b/resources/namespaces/__init__.py similarity index 100% rename from src/test_framework/__init__.py rename to resources/namespaces/__init__.py diff --git a/resources/namespaces/two_namespaces_two_users/namespace-defaults.yaml b/resources/namespaces/two_namespaces_two_users/namespace-defaults.yaml new file mode 100644 index 000000000..75cc8e42c --- /dev/null +++ b/resources/namespaces/two_namespaces_two_users/namespace-defaults.yaml @@ -0,0 +1,18 @@ +users: + - name: warnet-user + roles: + - pod-viewer + - pod-manager +# the pod-viewer and pod-manager roles are the default +# roles defined in values.yaml for the namespaces charts +# +# if you need a different set of roles for a particular namespaces +# deployment, you can override values.yaml by providing your own +# role definitions below +# +# roles: +# - name: my-custom-role +# rules: +# - apiGroups: "" +# resources: "" +# verbs: "" diff --git a/resources/namespaces/two_namespaces_two_users/namespaces.yaml b/resources/namespaces/two_namespaces_two_users/namespaces.yaml new file mode 100644 index 000000000..542456ef6 --- /dev/null +++ b/resources/namespaces/two_namespaces_two_users/namespaces.yaml @@ -0,0 +1,19 @@ +namespaces: + - name: wargames-red-team + users: + - name: alice + roles: + - pod-viewer + - name: bob + roles: + - pod-viewer + - pod-manager + - name: wargames-blue-team + users: + - name: mallory + roles: + - pod-viewer + - name: carol + roles: + - pod-viewer + - pod-manager diff --git a/src/warnet/backend/__init__.py b/resources/networks/__init__.py similarity index 100% rename from src/warnet/backend/__init__.py rename to resources/networks/__init__.py diff --git a/src/warnet/cli/__init__.py b/resources/plugins/__init__.py similarity index 100% rename from src/warnet/cli/__init__.py rename to resources/plugins/__init__.py diff --git a/resources/plugins/simln/README.md b/resources/plugins/simln/README.md new file mode 100644 index 000000000..2125306d2 --- /dev/null +++ b/resources/plugins/simln/README.md @@ -0,0 +1,123 @@ +# SimLN Plugin + +## SimLN +SimLN helps you generate lightning payment activity. + +* Website: https://simln.dev/ +* Github: https://github.com/bitcoin-dev-project/sim-ln + +## Usage +SimLN uses "activity" definitions to create payment activity between lightning nodes. These definitions are in JSON format. + +SimLN also requires access details for each node; however, the SimLN plugin will automatically generate these access details for each LND node. The access details look like this: + +```` JSON +{ + "id": , + "address": https://, + "macaroon": , + "cert": +} +```` +SimLN plugin also supports Core Lightning (CLN). CLN nodes connection details are transfered from the CLN node to SimLN node during launch-activity processing. +```` JSON +{ + "id": , + "address": https://, + "ca_cert": /working/-ca.pem, + "client_cert": /working/-client.pem, + "client_key": /working/-client-key.pem +} +```` + +Since SimLN already has access to those LND and CLN connection details, it means you can focus on the "activity" definitions. + +### Launch activity definitions from the command line +The SimLN plugin takes "activity" definitions like so: + +`./simln/plugin.py launch-activity '[{\"source\": \"tank-0003-ln\", \"destination\": \"tank-0005-ln\", \"interval_secs\": 1, \"amount_msat\": 2000}]'"''` + +### Launch activity definitions from within `network.yaml` +When you initialize a new Warnet network, Warnet will create a new `network.yaml` file. If your `network.yaml` file includes lightning nodes, then you can use SimLN to produce activity between those nodes like this: + +
+network.yaml + +````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: + config: | + bitcoin.timelockdelta=33 + channels: + - id: + block: 300 + index: 1 + target: tank-0004-ln + capacity: 100000 + push_amt: 50000 + + - name: tank-0004 + addnode: + - tank-0000 + ln: + cln: true + cln: + channels: + - id: + block: 300 + index: 2 + target: tank-0005-ln + capacity: 50000 + push_amt: 25000 + + - name: tank-0005 + addnode: + - tank-0000 + ln: + lnd: true + +plugins: + postDeploy: + simln: + entrypoint: "/path/to/plugins/simln" # This is the path to the simln plugin folder (relative to the network.yaml file). + activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' +```` + +
+ + +## Generating your own SimLN image +The SimLN plugin fetches a SimLN docker image from dockerhub. You can generate your own docker image if you choose: + +1. Clone SimLN: `git clone git@github.com:bitcoin-dev-project/sim-ln.git` +2. Follow the instructions to build a docker image as detailed in the SimLN repository. +3. Tag the resulting docker image: `docker tag IMAGEID YOURUSERNAME/sim-ln:VERSION` +4. Push the tagged image to your dockerhub account. +5. Modify the `values.yaml` file in the plugin's chart to reflect your username and version number: +```YAML + repository: "YOURUSERNAME/sim-ln" + tag: "VERSION" +``` diff --git a/resources/plugins/simln/charts/simln/.helmignore b/resources/plugins/simln/charts/simln/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/resources/plugins/simln/charts/simln/.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/simln/charts/simln/Chart.yaml b/resources/plugins/simln/charts/simln/Chart.yaml new file mode 100644 index 000000000..3df6dd232 --- /dev/null +++ b/resources/plugins/simln/charts/simln/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: simln +description: A Helm chart to deploy SimLN +version: 0.1.0 +appVersion: "0.1.0" diff --git a/resources/plugins/simln/charts/simln/templates/NOTES.txt b/resources/plugins/simln/charts/simln/templates/NOTES.txt new file mode 100644 index 000000000..74486845f --- /dev/null +++ b/resources/plugins/simln/charts/simln/templates/NOTES.txt @@ -0,0 +1 @@ +Thank you for installing SimLN. diff --git a/resources/plugins/simln/charts/simln/templates/_helpers.tpl b/resources/plugins/simln/charts/simln/templates/_helpers.tpl new file mode 100644 index 000000000..a699083e5 --- /dev/null +++ b/resources/plugins/simln/charts/simln/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/simln/charts/simln/templates/configmap.yaml b/resources/plugins/simln/charts/simln/templates/configmap.yaml new file mode 100644 index 000000000..8ea50e676 --- /dev/null +++ b/resources/plugins/simln/charts/simln/templates/configmap.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "mychart.fullname" . }}-data +data: + tls.cert: | + -----BEGIN CERTIFICATE----- + MIIB+DCCAZ6gAwIBAgIUSbyK/9viFWS3cLoPkmxZsW8fcH8wCgYIKoZIzj0EAwIw + MjEfMB0GA1UECgwWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2Fy + bmV0MB4XDTI1MDkwMzE1NDgzNFoXDTM1MDkwMTE1NDgzNFowMjEfMB0GA1UECgwW + bG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2FybmV0MFkwEwYHKoZI + zj0CAQYIKoZIzj0DAQcDQgAENIGvS4bQr/zzUQnIqgJIYrPEdPMXVkv3yEyJRCFg + PyZTvxWUJy7AI3VKb7ubIXawYcnPBe7K1sgBAbTPz1c8sqOBkTCBjjAOBgNVHQ8B + Af8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAd + BgNVHQ4EFgQUNhDWW7rajlA9sNGI/1Q5BDLH/rMwNwYDVR0RBDAwLoIJbG9jYWxo + b3N0ggkqLmRlZmF1bHSHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwCgYIKoZIzj0E + AwIDSAAwRQIhAOFm85wvwPZMJg0+16Sh0FkKqAuGVmllHnriWHQJ1NhuAiAfoxzE + 9ooZuDwKy0Y3dP4DfJCrOlFNTHfp3abG7VQ+VQ== + -----END CERTIFICATE----- + +{{- $configMaps := lookup "v1" "ConfigMap" .Release.Namespace "" }} +{{- range $configMaps.items }} + {{- if and .metadata.labels (hasKey .metadata.labels "role") (eq (index .metadata.labels "role") "macaroon-ref") }} + admin.macaroon.hex: {{ index .data "MACAROON_HEX" | quote }} + {{- end }} +{{- end }} \ No newline at end of file diff --git a/resources/plugins/simln/charts/simln/templates/pod.yaml b/resources/plugins/simln/charts/simln/templates/pod.yaml new file mode 100644 index 000000000..69790c9eb --- /dev/null +++ b/resources/plugins/simln/charts/simln/templates/pod.yaml @@ -0,0 +1,50 @@ +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: + - > + cp /configmap/* /working && + cd /working && + cat admin.macaroon.hex | xxd -r -p > admin.macaroon && + while [ ! -f /working/sim.json ]; do + echo "Waiting for /working/sim.json to exist..." + sleep 1 + done + volumeMounts: + - name: {{ .Values.workingVolume.name }} + mountPath: {{ .Values.workingVolume.mountPath }} + - name: {{ .Values.configmapVolume.name }} + mountPath: {{ .Values.configmapVolume.mountPath }} + containers: + - name: {{ .Values.name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - "sh" + - "-c" + args: + - > + cd /working; + sim-cli + volumeMounts: + - name: {{ .Values.workingVolume.name }} + mountPath: {{ .Values.workingVolume.mountPath }} + - name: {{ .Values.configmapVolume.name }} + mountPath: {{ .Values.configmapVolume.mountPath }} + volumes: + - name: {{ .Values.configmapVolume.name }} + configMap: + name: {{ include "mychart.fullname" . }}-data + - name: {{ .Values.workingVolume.name }} + emptyDir: {} diff --git a/resources/plugins/simln/charts/simln/values.yaml b/resources/plugins/simln/charts/simln/values.yaml new file mode 100644 index 000000000..a1647a963 --- /dev/null +++ b/resources/plugins/simln/charts/simln/values.yaml @@ -0,0 +1,13 @@ +name: "simln" +image: + repository: "bitcoindevproject/simln" + tag: "0.2.3" + pullPolicy: IfNotPresent + +workingVolume: + name: working-volume + mountPath: /working +configmapVolume: + name: configmap-volume + mountPath: /configmap + diff --git a/resources/plugins/simln/plugin.py b/resources/plugins/simln/plugin.py new file mode 100755 index 000000000..78df1a917 --- /dev/null +++ b/resources/plugins/simln/plugin.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +import json +import logging +import time +from enum import Enum +from pathlib import Path +from typing import Optional + +import click +from kubernetes.stream import stream + +from warnet.constants import LIGHTNING_MISSION, PLUGIN_ANNEX, AnnexMember, HookValue, WarnetContent +from warnet.k8s import ( + copyfile, + download, + get_default_namespace, + get_mission, + get_static_client, + wait_for_init, + write_file_to_container, +) +from warnet.process import run_command + +MISSION = "simln" +PRIMARY_CONTAINER = MISSION + +PLUGIN_DIR_TAG = "plugin_dir" + + +class PluginError(Exception): + pass + + +log = logging.getLogger(MISSION) +log.setLevel(logging.DEBUG) +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) + + +class PluginContent(Enum): + ACTIVITY = "activity" + + +@click.group() +@click.pass_context +def simln(ctx): + """Commands for the SimLN plugin""" + ctx.ensure_object(dict) + plugin_dir = Path(__file__).resolve().parent + ctx.obj[PLUGIN_DIR_TAG] = Path(plugin_dir) + + +@simln.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""" + # write your plugin startup commands here + activity = plugin_content.get(PluginContent.ACTIVITY.value) + if activity: + activity = json.loads(activity) + print(activity) + _launch_activity(activity, ctx.obj.get(PLUGIN_DIR_TAG)) + + +@simln.command() +def list_pod_names(): + """Get a list of SimLN pod names""" + print([pod.metadata.name for pod in get_mission(MISSION)]) + + +@simln.command() +@click.argument("pod_name", type=str) +def download_results(pod_name: str): + """Download SimLN results to the current directory""" + dest = download(pod_name, source_path=Path("/working/results")) + print(f"Downloaded results to: {dest}") + + +def _get_example_activity() -> list[dict]: + pods = get_mission(LIGHTNING_MISSION) + try: + pod_a = pods[1].metadata.name + pod_b = pods[2].metadata.name + except Exception as err: + raise PluginError( + "Could not access the lightning nodes needed for the example.\n Try deploying some." + ) from err + return [{"source": pod_a, "destination": pod_b, "interval_secs": 1, "amount_msat": 2000}] + + +@simln.command() +def get_example_activity(): + """Get an activity representing node 2 sending msat to node 3""" + print(json.dumps(_get_example_activity())) + + +@simln.command() +@click.argument(PluginContent.ACTIVITY.value, type=str) +@click.pass_context +def launch_activity(ctx, activity: str): + """Deploys a SimLN Activity which is a JSON list of objects""" + try: + parsed_activity = json.loads(activity) + except json.JSONDecodeError: + log.error("Invalid JSON input for activity.") + raise click.BadArgumentUsage("Activity must be a valid JSON string.") from None + plugin_dir = ctx.obj.get(PLUGIN_DIR_TAG) + print(_launch_activity(parsed_activity, plugin_dir)) + + +def _launch_activity(activity: Optional[list[dict]], plugin_dir: str) -> str: + """Launch a SimLN chart which optionally includes the `activity`""" + timestamp = int(time.time()) + name = f"simln-{timestamp}" + + command = f"helm upgrade --install {timestamp} {plugin_dir}/charts/simln" + + run_command(command) + activity_json = _generate_activity_json(activity) + wait_for_init(name, namespace=get_default_namespace(), quiet=True) + + # write cert files to container + transfer_cln_certs(name) + if write_file_to_container( + name, + "init", + "/working/sim.json", + activity_json, + namespace=get_default_namespace(), + quiet=True, + ): + return name + else: + raise PluginError(f"Could not write sim.json to the init container: {name}") + + +def _generate_activity_json(activity: Optional[list[dict]]) -> str: + nodes = [] + + for i in get_mission(LIGHTNING_MISSION): + ln_name = i.metadata.name + port = 10009 + node = {"id": ln_name} + if "cln" in i.metadata.labels["app.kubernetes.io/name"]: + port = 9736 + node["ca_cert"] = f"/working/{ln_name}-ca.pem" + node["client_cert"] = f"/working/{ln_name}-client.pem" + node["client_key"] = f"/working/{ln_name}-client-key.pem" + else: + node["macaroon"] = "/working/admin.macaroon" + node["cert"] = "/working/tls.cert" + node["address"] = f"https://{ln_name}:{port}" + nodes.append(node) + + if activity: + data = {"nodes": nodes, PluginContent.ACTIVITY.value: activity} + else: + data = {"nodes": nodes} + + return json.dumps(data, indent=2) + + +def transfer_cln_certs(name): + dst_container = "init" + for i in get_mission(LIGHTNING_MISSION): + ln_name = i.metadata.name + if "cln" in i.metadata.labels["app.kubernetes.io/name"]: + chain = i.metadata.labels["chain"] + cln_root = f"/root/.lightning/{chain}" + copyfile( + ln_name, + "cln", + f"{cln_root}/ca.pem", + name, + dst_container, + f"/working/{ln_name}-ca.pem", + ) + copyfile( + ln_name, + "cln", + f"{cln_root}/client.pem", + name, + dst_container, + f"/working/{ln_name}-client.pem", + ) + copyfile( + ln_name, + "cln", + f"{cln_root}/client-key.pem", + name, + dst_container, + f"/working/{ln_name}-client-key.pem", + ) + + +def _sh(pod, method: str, params: tuple[str, ...]) -> str: + namespace = get_default_namespace() + + sclient = get_static_client() + if params: + cmd = [method] + cmd.extend(params) + else: + cmd = [method] + try: + resp = stream( + sclient.connect_get_namespaced_pod_exec, + pod, + namespace, + container=PRIMARY_CONTAINER, + command=cmd, + stderr=True, + stdin=False, + stdout=True, + tty=False, + _preload_content=False, + ) + stdout = "" + stderr = "" + while resp.is_open(): + resp.update(timeout=1) + if resp.peek_stdout(): + stdout_chunk = resp.read_stdout() + stdout += stdout_chunk + if resp.peek_stderr(): + stderr_chunk = resp.read_stderr() + stderr += stderr_chunk + return stdout + stderr + except Exception as err: + print(f"Could not execute stream: {err}") + + +@simln.command(context_settings={"ignore_unknown_options": True}) +@click.argument("pod", type=str) +@click.argument("method", type=str) +@click.argument("params", type=str, nargs=-1) # this will capture all remaining arguments +def sh(pod: str, method: str, params: tuple[str, ...]): + """Run shell commands in a pod""" + print(_sh(pod, method, params)) + + +if __name__ == "__main__": + simln() diff --git a/resources/plugins/tor/charts/torda/.helmignore b/resources/plugins/tor/charts/torda/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/resources/plugins/tor/charts/torda/.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/tor/charts/torda/Chart.yaml b/resources/plugins/tor/charts/torda/Chart.yaml new file mode 100644 index 000000000..d7688b1ee --- /dev/null +++ b/resources/plugins/tor/charts/torda/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: torda +description: A Helm chart to deploy a Tor Directory Authority +version: 0.1.0 +appVersion: "0.1.0" diff --git a/resources/plugins/tor/charts/torda/templates/NOTES.txt b/resources/plugins/tor/charts/torda/templates/NOTES.txt new file mode 100644 index 000000000..30a130268 --- /dev/null +++ b/resources/plugins/tor/charts/torda/templates/NOTES.txt @@ -0,0 +1 @@ +Thank you for installing TorDA. diff --git a/resources/plugins/tor/charts/torda/templates/_helpers.tpl b/resources/plugins/tor/charts/torda/templates/_helpers.tpl new file mode 100644 index 000000000..f255490c8 --- /dev/null +++ b/resources/plugins/tor/charts/torda/templates/_helpers.tpl @@ -0,0 +1,46 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "torda.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "torda.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s" .Release.Name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "torda.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "torda.labels" -}} +helm.sh/chart: {{ include "torda.chart" . }} +{{ include "torda.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "torda.selectorLabels" -}} +app.kubernetes.io/name: {{ include "torda.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/resources/plugins/tor/charts/torda/templates/pod.yaml b/resources/plugins/tor/charts/torda/templates/pod.yaml new file mode 100644 index 000000000..4739dabc2 --- /dev/null +++ b/resources/plugins/tor/charts/torda/templates/pod.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "torda.fullname" . }} + labels: + {{- include "torda.labels" . | nindent 4 }} + app: {{ include "torda.fullname" . }} +spec: + containers: + - name: {{ .Values.name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: toror + containerPort: 9001 + protocol: TCP + - name: torda + containerPort: 9030 + protocol: TCP \ No newline at end of file diff --git a/resources/plugins/tor/charts/torda/templates/service.yaml b/resources/plugins/tor/charts/torda/templates/service.yaml new file mode 100644 index 000000000..8b920fc7b --- /dev/null +++ b/resources/plugins/tor/charts/torda/templates/service.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "torda.fullname" . }} + labels: + app: {{ include "torda.fullname" . }} +spec: + type: ClusterIP + ports: + - port: 9001 + targetPort: 9001 + protocol: TCP + name: toror + - port: 9030 + targetPort: 9030 + protocol: TCP + name: torda + selector: + {{- include "torda.selectorLabels" . | nindent 4 }} + diff --git a/resources/plugins/tor/charts/torda/values.yaml b/resources/plugins/tor/charts/torda/values.yaml new file mode 100644 index 000000000..3a9376f2e --- /dev/null +++ b/resources/plugins/tor/charts/torda/values.yaml @@ -0,0 +1,5 @@ +name: "torda" +image: + repository: "bitcoindevproject/tor-da" + tag: "latest" + pullPolicy: IfNotPresent diff --git a/resources/plugins/tor/plugin.py b/resources/plugins/tor/plugin.py new file mode 100644 index 000000000..cfc0803a2 --- /dev/null +++ b/resources/plugins/tor/plugin.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +from pathlib import Path + +from warnet.process import run_command + +if __name__ == "__main__": + command = f"helm upgrade --install torda {Path(__file__).parent / 'charts' / 'torda'}" + run_command(command) diff --git a/src/warnet/scenarios/__init__.py b/resources/scenarios/__init__.py similarity index 100% rename from src/warnet/scenarios/__init__.py rename to resources/scenarios/__init__.py diff --git a/resources/scenarios/commander.py b/resources/scenarios/commander.py new file mode 100644 index 000000000..a6cb541d1 --- /dev/null +++ b/resources/scenarios/commander.py @@ -0,0 +1,727 @@ +import argparse +import base64 +import configparser +import json +import logging +import os +import pathlib +import random +import signal +import struct +import sys +import tempfile +import threading +import types +from time import sleep + +from kubernetes import client, config +from kubernetes.stream import stream +from ln_framework.ln import CLN, LND, LNNode +from test_framework.authproxy import AuthServiceProxy +from test_framework.blocktools import get_witness_script, script_BIP34_coinbase_height +from test_framework.messages import ( + CBlock, + CBlockHeader, + COutPoint, + CTransaction, + CTxIn, + CTxInWitness, + CTxOut, + from_binary, + from_hex, + hash256, + ser_string, + ser_uint256, + tx_from_hex, +) +from test_framework.p2p import MAGIC_BYTES, NetworkThread +from test_framework.psbt import ( + PSBT, + PSBT_GLOBAL_UNSIGNED_TX, + PSBT_IN_FINAL_SCRIPTSIG, + PSBT_IN_FINAL_SCRIPTWITNESS, + PSBT_IN_NON_WITNESS_UTXO, + PSBT_IN_SIGHASH_TYPE, + PSBTMap, +) +from test_framework.script import CScriptOp +from test_framework.test_framework import ( + TMPDIR_PREFIX, + BitcoinTestFramework, + TestStatus, +) +from test_framework.test_node import TestNode +from test_framework.util import PortSeed, get_rpc_proxy + +SIGNET_HEADER = b"\xec\xc7\xda\xa2" +PSBT_SIGNET_BLOCK = ( + b"\xfc\x06signetb" # proprietary PSBT global field holding the block being signed +) + +NAMESPACE = None +pods = client.V1PodList(items=[]) +cmaps = client.V1ConfigMapList(items=[]) + +try: + # Get the in-cluster k8s client to determine what we have access to + config.load_incluster_config() + sclient = client.CoreV1Api() + + # Figure out what namespace we are in + with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace") as f: + NAMESPACE = f.read().strip() + + try: + # An admin with cluster access can list everything. + # A wargames player with namespaced access will get a FORBIDDEN error here + pods = sclient.list_pod_for_all_namespaces() + cmaps = sclient.list_config_map_for_all_namespaces() + except Exception: + # Just get whatever we have access to in this namespace only + pods = sclient.list_namespaced_pod(namespace=NAMESPACE) + cmaps = sclient.list_namespaced_config_map(namespace=NAMESPACE) +except Exception: + # If there is no cluster config, the user might just be + # running the scenario file locally with --help + pass + +WARNET = {"tanks": [], "lightning": [], "channels": []} +for pod in pods.items: + if "mission" not in pod.metadata.labels: + continue + + if pod.metadata.labels["mission"] == "tank": + WARNET["tanks"].append( + { + "tank": pod.metadata.name, + "namespace": pod.metadata.namespace, + "chain": pod.metadata.labels["chain"], + "p2pport": int(pod.metadata.labels["P2PPort"]), + "rpc_host": pod.status.pod_ip, + "rpc_port": int(pod.metadata.labels["RPCPort"]), + "rpc_user": "user", + "rpc_password": pod.metadata.labels["rpcpassword"], + "init_peers": pod.metadata.annotations["init_peers"], + } + ) + + if pod.metadata.labels["mission"] == "lightning": + if "lnd" in pod.metadata.labels["app.kubernetes.io/name"]: + lnnode = LND( + pod.metadata.name, + pod.metadata.namespace, + pod.status.pod_ip, + pod.metadata.annotations["adminMacaroon"], + ) + if "cln" in pod.metadata.labels["app.kubernetes.io/name"]: + lnnode = CLN(pod.metadata.name, pod.metadata.namespace, pod.status.pod_ip) + assert lnnode + WARNET["lightning"].append(lnnode) + +for cm in cmaps.items: + if not cm.metadata.labels or "channels" not in cm.metadata.labels: + continue + channel_jsons = json.loads(cm.data["channels"]) + for channel_json in channel_jsons: + channel_json["source"] = cm.data["source"] + WARNET["channels"].append(channel_json) + + +# Ensure that all RPC calls are made with brand new http connections +def auth_proxy_request(self, method, path, postdata): + self._set_conn() # creates new http client connection + return self.oldrequest(method, path, postdata) + + +AuthServiceProxy.oldrequest = AuthServiceProxy._request +AuthServiceProxy._request = auth_proxy_request + + +# Create a custom formatter +class ColorFormatter(logging.Formatter): + """Custom formatter to add color based on log level.""" + + # Define ANSI color codes + RED = "\033[91m" + YELLOW = "\033[93m" + GREEN = "\033[92m" + RESET = "\033[0m" + + FORMATS = { + logging.DEBUG: f"{RESET}%(name)-8s - Thread-%(thread)d - %(message)s{RESET}", + logging.INFO: f"{RESET}%(name)-8s - %(message)s{RESET}", + logging.WARNING: f"{YELLOW}%(name)-8s - %(message)s{RESET}", + logging.ERROR: f"{RED}%(name)-8s - %(message)s{RESET}", + logging.CRITICAL: f"{RED}##%(name)-8s - %(message)s##{RESET}", + } + + def format(self, record): + log_fmt = self.FORMATS.get(record.levelno) + formatter = logging.Formatter(log_fmt) + return formatter.format(record) + + +class Commander(BitcoinTestFramework): + # required by subclasses of BitcoinTestFramework + def set_test_params(self): + pass + + def run_test(self): + pass + + # Utility functions for Warnet scenarios + @staticmethod + def ensure_miner(node): + wallets = node.listwallets() + if "miner" not in wallets: + node.createwallet("miner", descriptors=True) + return node.get_wallet_rpc("miner") + + @staticmethod + def hex_to_b64(hex): + return base64.b64encode(bytes.fromhex(hex)).decode() + + @staticmethod + def b64_to_hex(b64, reverse=False): + if reverse: + return base64.b64decode(b64)[::-1].hex() + else: + return base64.b64decode(b64).hex() + + def wait_for_tanks_connected(self): + def tank_connected(self, tank): + while True: + peers = tank.getpeerinfo() + count = sum( + 1 + for peer in peers + if peer.get("connection_type") == "manual" or peer.get("addnode") is True + ) + self.log.info(f"Tank {tank.tank} connected to {count}/{tank.init_peers} peers") + if count >= tank.init_peers: + break + else: + sleep(5) + + conn_threads = [ + threading.Thread(target=tank_connected, args=(self, tank)) for tank in self.nodes + ] + for thread in conn_threads: + thread.start() + + all(thread.join() is None for thread in conn_threads) + self.log.info("Network connected") + + def handle_sigterm(self, signum, frame): + print("SIGTERM received, stopping...") + self.shutdown() + sys.exit(0) + + # The following functions are chopped-up hacks of + # the original methods from BitcoinTestFramework + + def setup(self): + signal.signal(signal.SIGTERM, self.handle_sigterm) + + # hacked from _start_logging() + # Scenarios will log plain messages to stdout only, which will can redirected by warnet + self.log = logging.getLogger(self.__class__.__name__) + self.log.setLevel(logging.INFO) # set this to DEBUG to see ALL RPC CALLS + + # Because scenarios run in their own subprocess, the logger here + # is not the same as the warnet server or other global loggers. + # Scenarios log directly to stdout which gets picked up by the + # subprocess manager in the server, and reprinted to the global log. + ch = logging.StreamHandler(sys.stdout) + ch.setFormatter(ColorFormatter()) + self.log.addHandler(ch) + + # Keep a separate index of tanks by pod name + self.tanks: dict[str, TestNode] = {} + self.lns: dict[str, LNNode] = {} + self.channels = WARNET["channels"] + + self.binary_paths = types.SimpleNamespace() + self.binary_paths.bitcoin_cmd = None + self.binary_paths.bitcoind = None + + for i, tank in enumerate(WARNET["tanks"]): + self.log.info( + f"Adding TestNode #{i} from pod {tank['tank']} with IP {tank['rpc_host']}" + ) + node = TestNode( + i, + pathlib.Path(), # datadir path + chain=tank["chain"], + rpchost=tank["rpc_host"], + timewait=self.rpc_timeout, + timeout_factor=self.options.timeout_factor, + binaries=self.get_binaries(), + cwd=self.options.tmpdir, + coverage_dir=self.options.coveragedir, + ) + node.tank = tank["tank"] + node._rpc = get_rpc_proxy( + f"http://{tank['rpc_user']}:{tank['rpc_password']}@{tank['rpc_host']}:{tank['rpc_port']}", + i, + timeout=self.rpc_timeout, + coveragedir=self.options.coveragedir, + ) + node.rpc_connected = True + node.init_peers = int(tank["init_peers"]) + node.p2pport = tank["p2pport"] + + self.nodes.append(node) + self.tanks[tank["tank"]] = node + + self.ln_nodes = [] + for ln in WARNET["lightning"]: + self.ln_nodes.append(ln) + self.lns[ln.name] = ln + + self.num_nodes = len(self.nodes) + + # Set up temp directory and start logging + if self.options.tmpdir: + self.options.tmpdir = os.path.abspath(self.options.tmpdir) + os.makedirs(self.options.tmpdir, exist_ok=False) + else: + self.options.tmpdir = tempfile.mkdtemp(prefix=TMPDIR_PREFIX) + + seed = self.options.randomseed + if seed is None: + seed = random.randrange(sys.maxsize) + else: + self.log.info(f"User supplied random seed {seed}") + random.seed(seed) + self.log.info(f"PRNG seed is: {seed}") + + self.log.debug("Setting up network thread") + self.network_thread = NetworkThread() + self.network_thread.start() + + self.success = TestStatus.PASSED + + if len(self.nodes) > 0 and self.nodes[0].chain == "signet": + # There's no garuntee that any nodes are responsive + # but we only need one to figure out the network magic bytes + for node in self.nodes: + try: + # Times out after 60 seconds (!) + template = node.getblocktemplate({"rules": ["segwit", "signet"]}) + challenge = template["signet_challenge"] + challenge_bytes = bytes.fromhex(challenge) + data = len(challenge_bytes).to_bytes() + challenge_bytes + digest = hash256(data) + MAGIC_BYTES["signet"] = digest[0:4] + self.log.info( + f"Got signet network magic bytes from {node.tank}: {MAGIC_BYTES['signet'].hex()}" + ) + break + except Exception as e: + self.log.info(f"Failed to get signet network magic bytes from {node.tank}: {e}") + + def parse_args(self, _): + # Only print "outer" args from parent class when using --help + help_parser = argparse.ArgumentParser(usage="%(prog)s [options]") + self.add_options(help_parser) + help_args, _ = help_parser.parse_known_args() + # Check if 'help' attribute exists in help_args before accessing it + if hasattr(help_args, "help") and help_args.help: + help_parser.print_help() + sys.exit(0) + + previous_releases_path = "" + parser = argparse.ArgumentParser(usage="%(prog)s [options]") + parser.add_argument( + "--nocleanup", + dest="nocleanup", + default=False, + action="store_true", + help="Leave bitcoinds and test.* datadir on exit or error", + ) + parser.add_argument( + "--nosandbox", + dest="nosandbox", + default=False, + action="store_true", + help="Don't use the syscall sandbox", + ) + parser.add_argument( + "--noshutdown", + dest="noshutdown", + default=False, + action="store_true", + help="Don't stop bitcoinds after the test execution", + ) + parser.add_argument( + "--cachedir", + dest="cachedir", + default=None, + help="Directory for caching pregenerated datadirs (default: %(default)s)", + ) + parser.add_argument( + "--tmpdir", dest="tmpdir", default=None, help="Root directory for datadirs" + ) + parser.add_argument( + "-l", + "--loglevel", + dest="loglevel", + default="DEBUG", + help="log events at this level and higher to the console. Can be set to DEBUG, INFO, WARNING, ERROR or CRITICAL. Passing --loglevel DEBUG will output all logs to console. Note that logs at all levels are always written to the test_framework.log file in the temporary test directory.", + ) + parser.add_argument( + "--tracerpc", + dest="trace_rpc", + default=False, + action="store_true", + help="Print out all RPC calls as they are made", + ) + parser.add_argument( + "--portseed", + dest="port_seed", + default=0, + help="The seed to use for assigning port numbers (default: current process id)", + ) + parser.add_argument( + "--previous-releases", + dest="prev_releases", + default=None, + action="store_true", + help="Force test of previous releases (default: %(default)s)", + ) + parser.add_argument( + "--coveragedir", + dest="coveragedir", + default=None, + help="Write tested RPC commands into this directory", + ) + parser.add_argument( + "--configfile", + dest="configfile", + default=None, + help="Location of the test framework config file (default: %(default)s)", + ) + parser.add_argument( + "--pdbonfailure", + dest="pdbonfailure", + default=False, + action="store_true", + help="Attach a python debugger if test fails", + ) + parser.add_argument( + "--usecli", + dest="usecli", + default=False, + action="store_true", + help="use bitcoin-cli instead of RPC for all commands", + ) + parser.add_argument( + "--perf", + dest="perf", + default=False, + action="store_true", + help="profile running nodes with perf for the duration of the test", + ) + parser.add_argument( + "--valgrind", + dest="valgrind", + default=False, + action="store_true", + help="run nodes under the valgrind memory error detector: expect at least a ~10x slowdown. valgrind 3.14 or later required.", + ) + parser.add_argument( + "--randomseed", + default=0x7761726E6574, # "warnet" ascii + help="set a random seed for deterministically reproducing a previous test run", + ) + parser.add_argument( + "--timeout-factor", + dest="timeout_factor", + default=1, + type=float, + help="adjust test timeouts by a factor. Setting it to 0 disables all timeouts", + ) + parser.add_argument( + "--network", + dest="network", + default="warnet", + help="Designate which warnet this should run on (default: warnet)", + ) + parser.add_argument( + "--v2transport", + dest="v2transport", + default=False, + action="store_true", + help="use BIP324 v2 connections between all nodes by default", + ) + parser.add_argument( + "--test_methods", + dest="test_methods", + nargs="*", + help="Run specified test methods sequentially instead of the full test. Use only for methods that do not depend on any context set up in run_test or other methods.", + ) + + self.add_options(parser) + # Running TestShell in a Jupyter notebook causes an additional -f argument + # To keep TestShell from failing with an "unrecognized argument" error, we add a dummy "-f" argument + # source: https://stackoverflow.com/questions/48796169/how-to-fix-ipykernel-launcher-py-error-unrecognized-arguments-in-jupyter/56349168#56349168 + parser.add_argument("-f", "--fff", help="a dummy argument to fool ipython", default="1") + self.options = parser.parse_args() + if self.options.timeout_factor == 0: + self.options.timeout_factor = 999 + self.options.timeout_factor = self.options.timeout_factor or ( + 4 if self.options.valgrind else 1 + ) + self.options.previous_releases_path = previous_releases_path + config = configparser.ConfigParser() + if self.options.configfile is not None: + with open(self.options.configfile) as f: + config.read_file(f) + + config["environment"] = {"PACKAGE_BUGREPORT": ""} + + self.config = config + + if "descriptors" not in self.options: + # Wallet is not required by the test at all and the value of self.options.descriptors won't matter. + # It still needs to exist and be None in order for tests to work however. + # So set it to None to force -disablewallet, because the wallet is not needed. + self.options.descriptors = None + elif self.options.descriptors is None: + # Some wallet is either required or optionally used by the test. + # Prefer SQLite unless it isn't available + if self.is_sqlite_compiled(): + self.options.descriptors = True + elif self.is_bdb_compiled(): + self.options.descriptors = False + else: + # If neither are compiled, tests requiring a wallet will be skipped and the value of self.options.descriptors won't matter + # It still needs to exist and be None in order for tests to work however. + # So set it to None, which will also set -disablewallet. + self.options.descriptors = None + + PortSeed.n = self.options.port_seed + + def connect_nodes(self, a, b, *, peer_advertises_v2=None, wait_for_connect: bool = True): + """ + Kwargs: + wait_for_connect: if True, block until the nodes are verified as connected. You might + want to disable this when using -stopatheight with one of the connected nodes, + since there will be a race between the actual connection and performing + the assertions before one node shuts down. + """ + from_connection = self.nodes[a] + to_connection = self.nodes[b] + from_num_peers = 1 + len(from_connection.getpeerinfo()) + to_num_peers = 1 + len(to_connection.getpeerinfo()) + ip_port = f"{self.nodes[b].rpchost}:{self.nodes[b].p2pport}" + + if peer_advertises_v2 is None: + peer_advertises_v2 = self.options.v2transport + + if peer_advertises_v2: + from_connection.addnode(node=ip_port, command="onetry", v2transport=True) + else: + # skip the optional third argument (default false) for + # compatibility with older clients + from_connection.addnode(ip_port, "onetry") + + if not wait_for_connect: + return + + # poll until version handshake complete to avoid race conditions + # with transaction relaying + # See comments in net_processing: + # * Must have a version message before anything else + # * Must have a verack message before anything else + self.wait_until( + lambda: ( + sum(peer["version"] != 0 for peer in from_connection.getpeerinfo()) + == from_num_peers + ) + ) + self.wait_until( + lambda: ( + sum(peer["version"] != 0 for peer in to_connection.getpeerinfo()) == to_num_peers + ) + ) + self.wait_until( + lambda: ( + sum( + peer["bytesrecv_per_msg"].pop("verack", 0) >= 21 + for peer in from_connection.getpeerinfo() + ) + == from_num_peers + ) + ) + self.wait_until( + lambda: ( + sum( + peer["bytesrecv_per_msg"].pop("verack", 0) >= 21 + for peer in to_connection.getpeerinfo() + ) + == to_num_peers + ) + ) + # The message bytes are counted before processing the message, so make + # sure it was fully processed by waiting for a ping. + self.wait_until( + lambda: ( + sum( + peer["bytesrecv_per_msg"].pop("pong", 0) >= 29 + for peer in from_connection.getpeerinfo() + ) + == from_num_peers + ) + ) + self.wait_until( + lambda: ( + sum( + peer["bytesrecv_per_msg"].pop("pong", 0) >= 29 + for peer in to_connection.getpeerinfo() + ) + == to_num_peers + ) + ) + + def generatetoaddress(self, generator, n, addr, sync_fun=None, **kwargs): + if generator.chain == "regtest": + blocks = generator.generatetoaddress(n, addr, called_by_framework=True, **kwargs) + sync_fun() if sync_fun else self.sync_all() + return blocks + if generator.chain == "signet": + mined_blocks = 0 + block_hashes = [] + + def bcli(method, *args, **kwargs): + return generator.__getattr__(method)(*args, **kwargs) + + while mined_blocks < n: + # gbt + tmpl = bcli("getblocktemplate", {"rules": ["signet", "segwit"]}) + # address for reward + reward_spk = bytes.fromhex(bcli("getaddressinfo", addr)["scriptPubKey"]) + # create coinbase tx + cbtx = CTransaction() + cbtx.vin = [ + CTxIn( + COutPoint(0, 0xFFFFFFFF), + script_BIP34_coinbase_height(tmpl["height"]), + 0xFFFFFFFF, + ) + ] + cbtx.vout = [CTxOut(tmpl["coinbasevalue"], reward_spk)] + cbtx.vin[0].nSequence = 2**32 - 2 + + # assemble block + block = CBlock() + block.nVersion = tmpl["version"] + block.hashPrevBlock = int(tmpl["previousblockhash"], 16) + block.nTime = tmpl["curtime"] + if block.nTime < tmpl["mintime"]: + block.nTime = tmpl["mintime"] + block.nBits = int(tmpl["bits"], 16) + block.nNonce = 0 + block.vtx = [cbtx] + [tx_from_hex(t["data"]) for t in tmpl["transactions"]] + witnonce = 0 + witroot = block.calc_witness_merkle_root() + cbwit = CTxInWitness() + cbwit.scriptWitness.stack = [ser_uint256(witnonce)] + block.vtx[0].wit.vtxinwit = [cbwit] + block.vtx[0].vout.append(CTxOut(0, bytes(get_witness_script(witroot, witnonce)))) + # create signet txs for signing + signet_spk = tmpl["signet_challenge"] + signet_spk_bin = bytes.fromhex(signet_spk) + txs = block.vtx[:] + txs[0] = CTransaction(txs[0]) + txs[0].vout[-1].scriptPubKey += CScriptOp.encode_op_pushdata(SIGNET_HEADER) + hashes = [] + for tx in txs: + hashes.append(ser_uint256(tx.txid_int)) + mroot = block.get_merkle_root(hashes) + sd = b"" + sd += struct.pack(" 1 + + +# https://github.com/lightningcn/lightning-rfc/blob/master/07-routing-gossip.md#the-channel_update-message +# We use the field names as written in the BOLT as our canonical, internal field names. +# In LND, Policy objects returned by DescribeGraph have completely different labels +# than policy objects expected by the UpdateChannelPolicy API, and neither +# of these are the names used in the BOLT... +class Policy: + def __init__( + self, + cltv_expiry_delta: int, + htlc_minimum_msat: int, + fee_base_msat: int, + fee_proportional_millionths: int, + htlc_maximum_msat: int, + ): + self.cltv_expiry_delta = cltv_expiry_delta + self.htlc_minimum_msat = htlc_minimum_msat + self.fee_base_msat = fee_base_msat + self.fee_proportional_millionths = fee_proportional_millionths + self.htlc_maximum_msat = htlc_maximum_msat + + @classmethod + def from_lnd_describegraph(cls, policy: dict): + return cls( + cltv_expiry_delta=int(policy.get("time_lock_delta")), + htlc_minimum_msat=int(policy.get("min_htlc")), + fee_base_msat=int(policy.get("fee_base_msat")), + fee_proportional_millionths=int(policy.get("fee_rate_milli_msat")), + htlc_maximum_msat=int(policy.get("max_htlc_msat")), + ) + + @classmethod + def from_dict(cls, policy: dict): + return cls( + cltv_expiry_delta=policy.get("cltv_expiry_delta"), + htlc_minimum_msat=policy.get("htlc_minimum_msat"), + fee_base_msat=policy.get("fee_base_msat"), + fee_proportional_millionths=policy.get("fee_proportional_millionths"), + htlc_maximum_msat=policy.get("htlc_maximum_msat"), + ) + + def to_dict(self): + return { + "cltv_expiry_delta": self.cltv_expiry_delta, + "htlc_minimum_msat": self.htlc_minimum_msat, + "fee_base_msat": self.fee_base_msat, + "fee_proportional_millionths": self.fee_proportional_millionths, + "htlc_maximum_msat": self.htlc_maximum_msat, + } + + def to_lnd_chanpolicy(self, capacity): + # LND requires a 1% reserve + reserve = ((capacity * 99) // 100) * 1000 + # "min htlc amount of 0 mSAT is below min htlc parameter of 1 mSAT" + min_htlc = 1 + return { + "time_lock_delta": self.cltv_expiry_delta, + "min_htlc_msat": max(self.htlc_minimum_msat, min_htlc), + "base_fee_msat": self.fee_base_msat, + "fee_rate_ppm": self.fee_proportional_millionths, + "max_htlc_msat": min(self.htlc_maximum_msat, reserve), + "min_htlc_msat_specified": True, + } + + +class LNNode(ABC): + @abstractmethod + def __init__(self, pod_name, pod_namespace, ip_address): + self.name = pod_name + self.namespace = pod_namespace + self.ip_address = ip_address + self.log = logging.getLogger(pod_name) + handler = logging.StreamHandler() + formatter = logging.Formatter("%(name)-8s - %(levelname)s: %(message)s") + handler.setFormatter(formatter) + self.log.addHandler(handler) + self.log.setLevel(logging.INFO) + + @staticmethod + def hex_to_b64(hex): + return base64.b64encode(bytes.fromhex(hex)).decode() + + @staticmethod + def b64_to_hex(b64, reverse=False): + if reverse: + return base64.b64decode(b64)[::-1].hex() + else: + return base64.b64decode(b64).hex() + + @abstractmethod + def newaddress(self) -> tuple[bool, str]: + pass + + @abstractmethod + def uri(self) -> str: + pass + + @abstractmethod + def walletbalance(self) -> int: + pass + + @abstractmethod + def connect(self, target_uri) -> dict: + pass + + @abstractmethod + def channel(self, pk, capacity, push_amt, fee_rate) -> dict: + pass + + @abstractmethod + def graph(self) -> dict: + pass + + @abstractmethod + def update(self, txid_hex: str, policy: dict, capacity: int) -> dict: + pass + + +class CLN(LNNode): + def __init__(self, pod_name, pod_namespace, ip_address): + super().__init__(pod_name, pod_namespace, ip_address) + self.conn = None + self.headers = {} + self.impl = "cln" + self.reset_connection() + + def reset_connection(self): + self.conn = http.client.HTTPSConnection( + host=f"{self.name}.{self.namespace}", port=3010, timeout=60, context=INSECURE_CONTEXT + ) + + def setRune(self, rune): + self.headers = {"Rune": rune} + + def get(self, uri): + self.reset_connection() + self.log.info(f"CLN GET headers: {self.headers}") + self.conn.request( + method="GET", + url=uri, + headers=self.headers, + ) + return self.conn.getresponse().read().decode("utf8") + + def post(self, uri, data=None): + if not data: + data = {} + body = json.dumps(data) + post_header = self.headers + post_header["Content-Length"] = str(len(body)) + post_header["Content-Type"] = "application/json" + self.reset_connection() + self.conn.request( + method="POST", + url=uri, + body=body, + headers=post_header, + ) + # Stream output, otherwise we get a timeout error + res = self.conn.getresponse() + stream = "" + while True: + try: + data = res.read(1) + if len(data) == 0: + break + else: + stream += data.decode("utf8") + except Exception: + break + return stream + + def createrune(self): + while True: + response = requests.get(f"http://{self.ip_address}:8080/rune.json", timeout=5).text + if not response: + self.log.warning(f"Unable to fetch rune from {self.name}, retrying in 2 seconds...") + sleep(2) + continue + self.log.debug(response) + res = json.loads(response) + self.setRune(res["rune"]) + return + + def newaddress(self): + self.createrune() + response = self.post("/v1/newaddr", data={"addresstype": "p2tr"}) + res = json.loads(response) + if "p2tr" in res: + return res["p2tr"] + raise Exception(res) + + def uri(self): + res = json.loads(self.post("/v1/getinfo")) + return f"{res['id']}@{res['address'][0]['address']}:{res['address'][0]['port']}" + + def walletbalance(self) -> int: + response = self.post("/v1/listfunds") + res = json.loads(response) + return int(sum(o["amount_msat"] for o in res["outputs"]) / 1000) + + def channelbalance(self) -> int: + response = self.post("/v1/listfunds") + res = json.loads(response) + return int(sum(o["our_amount_msat"] for o in res["channels"]) / 1000) + + def connect(self, target_uri) -> dict: + response = self.post("/v1/connect", {"id": target_uri}) + res = json.loads(response) + if "id" in res: + return {} + else: + return res + + def channel(self, pk, capacity, push_amt, fee_rate) -> dict: + data = { + "amount": capacity, + "push_msat": push_amt, + "id": pk, + "feerate": fee_rate, + } + response = self.post("/v1/fundchannel", data) + res = json.loads(response) + return {"txid": res["txid"], "outpoint": f"{res['txid']}:{res['outnum']}"} + + def createinvoice(self, sats, label) -> str: + response = self.post("invoice", {"amount_msat": sats * 1000, "label": label}) + res = json.loads(response) + return res["bolt11"] + + def payinvoice(self, payment_request) -> str: + response = self.post("/v1/pay", {"bolt11": payment_request}) + res = json.loads(response) + return res + + def graph(self) -> dict: + response = self.post("/v1/listchannels") + res = json.loads(response) + # Map to desired output + filtered_channels = [ch for ch in res["channels"] if ch["direction"] == 1] + # Sort by short_channel_id - block -> index -> output + sorted_channels = sorted(filtered_channels, key=lambda x: x["short_channel_id"]) + # Add capacity by dividing amount_msat by 1000 + for channel in sorted_channels: + channel["capacity"] = channel["amount_msat"] // 1000 + return {"edges": sorted_channels} + + def update(self, txid_hex: str, policy: dict, capacity: int) -> dict: + raise Exception("Channel Policy Updates not supported by CLN yet!") + + +class LND(LNNode): + def __init__(self, pod_name, pod_namespace, ip_address, admin_macaroon_hex): + super().__init__(pod_name, pod_namespace, ip_address) + self.conn = None + self.admin_macaroon_hex = admin_macaroon_hex + self.headers = { + "Grpc-Metadata-macaroon": admin_macaroon_hex, + "Connection": "close", + } + self.impl = "lnd" + + def reset_connection(self): + self.conn = http.client.HTTPSConnection( + host=f"{self.name}.{self.namespace}", port=8080, timeout=60, context=INSECURE_CONTEXT + ) + + def get(self, uri): + self.reset_connection() + self.conn.request( + method="GET", + url=uri, + headers=self.headers, + ) + return self.conn.getresponse().read().decode("utf8") + + def post(self, uri, data, wait_for_completion=True): + body = json.dumps(data) + post_header = self.headers + post_header["Content-Length"] = str(len(body)) + post_header["Content-Type"] = "application/json" + self.reset_connection() + self.conn.request( + method="POST", + url=uri, + body=body, + headers=post_header, + ) + # Stream output, otherwise we get a timeout error + res = self.conn.getresponse() + stream = "" + while True: + try: + data = res.read(1) + if len(data) == 0: + break + if not wait_for_completion and data.decode("utf8") == "\n": + break + stream += data.decode("utf8") + except Exception: + break + return stream + + def newaddress(self): + # Taproot signatures are a fixed length which improves + # the accuracy of fee estimation, and therefore our + # channel ID determinism. + response = self.get("/v1/newaddress?type=TAPROOT_PUBKEY") + res = json.loads(response) + if "address" in res: + return res["address"] + raise Exception(res) + + def walletbalance(self) -> int: + res = self.get("/v1/balance/blockchain") + return int(json.loads(res)["confirmed_balance"]) + + def channelbalance(self) -> int: + res = self.get("/v1/balance/channels") + return int(json.loads(res)["balance"]) + + def uri(self): + res = self.get("/v1/getinfo") + info = json.loads(res) + return info["uris"][0] + + def connect(self, target_uri): + pk, host = target_uri.split("@") + response = self.post("/v1/peers", data={"addr": {"pubkey": pk, "host": host}}) + res = json.loads(response) + if "status" in res and "initiated" in res["status"]: + return {} + else: + return res + + def channel(self, pk, capacity, push_amt, fee_rate): + b64_pk = self.hex_to_b64(pk) + response = self.post( + "/v1/channels/stream", + data={ + "local_funding_amount": capacity, + "push_sat": push_amt, + "node_pubkey": b64_pk, + "sat_per_vbyte": fee_rate, + }, + ) + res = json.loads(response) + if "result" not in res: + raise Exception(res) + res["txid"] = self.b64_to_hex(res["result"]["chan_pending"]["txid"], reverse=True) + res["outpoint"] = f"{res['txid']}:{res['result']['chan_pending']['output_index']}" + return res + + def update(self, txid_hex: str, policy: dict, capacity: int): + ln_policy = Policy.from_dict(policy).to_lnd_chanpolicy(capacity) + data = {"chan_point": {"funding_txid_str": txid_hex, "output_index": 0}, **ln_policy} + res = self.post( + "/v1/chanpolicy", + # Policy objects returned by DescribeGraph have + # completely different labels than policy objects expected + # by the UpdateChannelPolicy API. + data=data, + ) + return json.loads(res) + + def createinvoice(self, sats, label) -> str: + response = self.post("/v1/invoices", data={"value": sats, "memo": label}) + res = json.loads(response) + return res["payment_request"] + + def payinvoice(self, payment_request) -> str: + response = self.post( + "/v2/router/send", + data={"payment_request": payment_request, "fee_limit_sat": 2100000000}, + wait_for_completion=False, + ) + res = json.loads(response) + return res + + def graph(self): + res = self.get("/v1/graph") + return json.loads(res) diff --git a/resources/scenarios/ln_init.py b/resources/scenarios/ln_init.py new file mode 100644 index 000000000..31445d442 --- /dev/null +++ b/resources/scenarios/ln_init.py @@ -0,0 +1,518 @@ +#!/usr/bin/env python3 + +import threading +from time import sleep + +from commander import Commander +from ln_framework.ln import ( + CHANNEL_OPEN_START_HEIGHT, + CHANNEL_OPENS_PER_BLOCK, + FEE_RATE_DECREMENT, + MAX_FEE_RATE, + Policy, +) +from test_framework.address import address_to_scriptpubkey +from test_framework.messages import ( + COIN, + CTransaction, + CTxOut, +) + + +class LNInit(Commander): + def set_test_params(self): + self.num_nodes = None + + def add_options(self, parser): + parser.description = "Fund LN wallets and open channels" + parser.usage = "warnet run /path/to/ln_init.py" + parser.add_argument( + "--miner", + dest="miner", + type=str, + help="Select one tank by name as the blockchain miner", + ) + + def run_test(self): + ## + # L1 P2P + ## + self.log.info("Waiting for L1 p2p network connections...") + self.wait_for_tanks_connected() + + ## + # MINER + ## + self.log.info("Setting up miner...") + if self.options.miner: + self.log.info(f"Parsed 'miner' argument: {self.options.miner}") + mining_tank = self.tanks[self.options.miner] + elif "miner" in self.tanks: + # or choose the tank with the right name + self.log.info("Found tank named 'miner'") + mining_tank = self.tanks["miner"] + else: + mining_tank = self.nodes[0] + self.log.info(f"Using tank {mining_tank.tank} as miner") + + miner = self.ensure_miner(mining_tank) + miner_addr = miner.getnewaddress() + + def gen(n): + # Take all the time you need to generate those blocks + mining_tank.rpc_timeout = 6000 + return self.generatetoaddress(mining_tank, n, miner_addr, sync_fun=self.no_op) + + self.log.info("Locking out of IBD...") + gen(1) + + ## + # WALLET ADDRESSES + ## + self.log.info("Getting LN wallet addresses...") + ln_addrs = {} + + def get_ln_addr(self, ln): + while True: + try: + address = ln.newaddress() + ln_addrs[ln.name] = address + self.log.info(f"Got wallet address {address} from {ln.name}") + break + except Exception as e: + self.log.info( + f"Couldn't get wallet address from {ln.name} because {e}, retrying in 5 seconds..." + ) + sleep(5) + + addr_threads = [ + threading.Thread(target=get_ln_addr, args=(self, ln)) for ln in self.lns.values() + ] + for thread in addr_threads: + thread.start() + + all(thread.join() is None for thread in addr_threads) + self.log.info(f"Got {len(ln_addrs)} addresses from {len(self.lns)} LN nodes") + + ## + # FUNDS + ## + self.log.info("Funding LN wallets...") + # One past block generated already to lock out IBD + # One next block to consolidate the miner's coins + # One next block to confirm the distributed coins + # Then the channel open TXs go in the expected block height + gen(CHANNEL_OPEN_START_HEIGHT - 4) + # divvy up the goods, except fee. + # Multiple UTXOs per LN wallet so multiple channels can be opened per block + miner_balance = int(miner.getbalance()) + # To reduce individual TX weight, consolidate all outputs before distribution + miner.sendtoaddress(miner_addr, miner_balance - 1) + gen(1) + helicopter = CTransaction() + + # Provide the source LN node for each channel with a UTXO just big enough + # to open that channel with its capacity plus fee. + channel_openers = [] + for ch in self.channels: + if ch["source"] not in channel_openers: + channel_openers.append(ch["source"]) + addr = ln_addrs[ch["source"]] + # More than enough to open the channel plus fee and cover LND's "maxFeeRatio" + # As long as all channel capacities are < 4 BTC the change output will be + # larger and occupy tx output 1, leaving the actual channel open at output 0 + sat_amt = 10 * COIN + helicopter.vout.append(CTxOut(sat_amt, address_to_scriptpubkey(addr))) + rawtx = miner.fundrawtransaction(helicopter.serialize().hex()) + signed_tx = miner.signrawtransactionwithwallet(rawtx["hex"])["hex"] + txid = miner.sendrawtransaction(signed_tx) + # confirm funds in last block before channel opens + gen(1) + + txstats = miner.gettransaction(txid) + self.log.info( + "Funds distribution from miner:\n " + + f"txid: {txid}\n " + + f"# outputs: {len(txstats['details'])}\n " + + f"total amount: {txstats['amount']}\n " + + f"remaining miner balance: {miner.getbalance()}" + ) + + self.log.info("Waiting for funds to be spendable by channel-openers") + + def confirm_ln_balance(self, ln_name): + ln = self.lns[ln_name] + while True: + try: + bal = ln.walletbalance() + if bal >= 0: + self.log.info(f"LN node {ln_name} confirmed funds") + break + else: + self.log.info(f"Got 0 balance from {ln_name} retrying in 5 seconds...") + sleep(5) + except Exception as e: + self.log.info( + f"Couldn't get balance from {ln_name} because {e}, retrying in 5 seconds..." + ) + sleep(5) + + fund_threads = [ + threading.Thread(target=confirm_ln_balance, args=(self, ln_name)) + for ln_name in channel_openers + ] + for thread in fund_threads: + thread.start() + + all(thread.join() is None for thread in fund_threads) + self.log.info("All channel-opening LN nodes are funded") + + ## + # URIs + ## + self.log.info("Getting URIs for all LN nodes...") + ln_uris = {} + + def get_ln_uri(self, ln): + while True: + try: + uri = ln.uri() + ln_uris[ln.name] = uri + self.log.info(f"LN node {ln.name} has URI {uri}") + break + except Exception as e: + self.log.info( + f"Couldn't get URI from {ln.name} because {e}, retrying in 5 seconds..." + ) + sleep(5) + + uri_threads = [ + threading.Thread(target=get_ln_uri, args=(self, ln)) for ln in self.lns.values() + ] + for thread in uri_threads: + thread.start() + + all(thread.join() is None for thread in uri_threads) + self.log.info("Got URIs from all LN nodes") + + ## + # P2P CONNECTIONS + ## + self.log.info("Adding p2p connections to LN nodes...") + # (source: LND, target_uri: str) tuples of LND instances + connections = [] + # Cycle graph through all LN nodes + nodes = list(self.lns.values()) + prev_node = nodes[-1] + for node in nodes: + connections.append((node, prev_node)) + prev_node = node + # Explicit connections between every pair of channel partners + for ch in self.channels: + src = self.lns[ch["source"]] + tgt = self.lns[ch["target"]] + # Avoid duplicates and reciprocals + if (src, tgt) not in connections and (tgt, src) not in connections: + connections.append((src, tgt)) + + def connect_ln(self, pair): + while True: + try: + res = pair[0].connect(ln_uris[pair[1].name]) + if res == {}: + self.log.info(f"Connected LN nodes {pair[0].name} -> {pair[1].name}") + break + if "message" in res: + if "already connected" in res["message"]: + self.log.info( + f"Already connected LN nodes {pair[0].name} -> {pair[1].name}" + ) + break + if "process of starting" in res["message"]: + self.log.info( + f"{pair[0].name} not ready for connections yet, wait and retry..." + ) + sleep(5) + else: + raise Exception(res) + except Exception as e: + self.log.info( + f"Couldn't connect {pair[0].name} -> {pair[1].name} because {e}, retrying in 5 seconds..." + ) + sleep(5) + + p2p_threads = [ + threading.Thread(target=connect_ln, args=(self, pair)) for pair in connections + ] + for thread in p2p_threads: + sleep(0.25) + thread.start() + + all(thread.join() is None for thread in p2p_threads) + self.log.info("Established all LN p2p connections") + + ## + # CHANNELS + ## + self.log.info("Opening lightning channels...") + # Sort the channels by assigned block and index + # so their channel ids are deterministic + ch_by_block = {} + for ch in self.channels: + if "id" not in ch or "block" not in ch["id"]: + raise Exception(f"LN Channel {ch} not found") + block = ch["id"]["block"] + if block not in ch_by_block: + ch_by_block[block] = [ch] + else: + ch_by_block[block].append(ch) + blocks = list(ch_by_block.keys()) + blocks = sorted(blocks) + + for target_block in blocks: + # First make sure the target block is the next block + current_height = self.nodes[0].getblockcount() + need = target_block - current_height + if need < 1: + raise Exception("Blockchain too long for deterministic channel ID") + if need > 1: + gen(need - 1) + + def open_channel(self, ch, fee_rate): + src = self.lns[ch["source"]] + tgt_uri = ln_uris[ch["target"]] + tgt_pk, _ = tgt_uri.split("@") + log = f" {ch['source']} -> {ch['target']}\n {ch['id']} fee: {fee_rate}" + while True: + self.log.info(f"Sending channel open:\n{log}") + try: + res = src.channel( + pk=tgt_pk, + capacity=ch["capacity"], + push_amt=ch["push_amt"], + fee_rate=fee_rate, + ) + ch["txid"] = res["txid"] + ch["outpoint"] = res["outpoint"] + self.log.info( + f"Channel open success:\n{log}\n outpoint: {res['outpoint']}" + ) + break + except Exception as e: + self.log.info( + f"Couldn't open channel:\n{log}\n {e}\n Retrying in 5 seconds..." + ) + sleep(5) + + channels = sorted(ch_by_block[target_block], key=lambda ch: ch["id"]["index"]) + if len(channels) > CHANNEL_OPENS_PER_BLOCK: + raise Exception( + f"Too many channels in block {target_block}: {len(channels)} / Maximum: {CHANNEL_OPENS_PER_BLOCK}" + ) + index = 0 + fee_rate = MAX_FEE_RATE + ch_threads = [] + for ch in channels: + index += 1 # noqa + fee_rate -= FEE_RATE_DECREMENT + assert index == ch["id"]["index"], "Channel ID indexes are not consecutive" + assert fee_rate >= 1, "Too many TXs in block, out of fee range" + t = threading.Thread(target=open_channel, args=(self, ch, fee_rate)) + sleep(0.25) + t.start() + ch_threads.append(t) + + all(thread.join() is None for thread in ch_threads) + for ch in channels: + if ch["outpoint"][-2:] != ":0": + self.log.error(f"Channel open outpoint not tx output index 0\n {ch}") + raise Exception("Channel determinism ruined, abort!") + + self.log.info(f"Waiting for {len(channels)} channel opens in mempool...") + self.wait_until( + lambda channels=channels: self.nodes[0].getmempoolinfo()["size"] >= len(channels), + timeout=500, + ) + block_hash = gen(1)[0] + self.log.info(f"Confirmed {len(channels)} channel opens in block {target_block}") + self.log.info("Checking deterministic channel IDs in block...") + block = self.nodes[0].getblock(block_hash) + block_txs = block["tx"] + block_height = block["height"] + for ch in channels: + assert ch["txid"] != "N/A", f"Channel:{ch} did not receive txid" + assert ch["id"]["block"] == block_height, f"Actual block:{block_height}\n{ch}" + assert block_txs[ch["id"]["index"]] == ch["txid"], ( + f"Actual txid:{block_txs[ch['id']['index']]}\n{ch}" + ) + self.log.info("👍") + + gen(5) + self.log.info(f"Confirmed {len(self.channels)} total channel opens") + + self.log.info("Waiting for channel announcement gossip...") + + def ln_all_chs(self, ln): + expected = len(self.channels) + actual = 0 + while actual != expected: + try: + actual = len(ln.graph()["edges"]) + if actual == expected: + self.log.info(f"LN {ln.name} has graph with all {expected} channels") + break + else: + self.log.info( + f"LN {ln.name} graph is incomplete - {actual} of {expected} channels, checking again in 5 seconds..." + ) + sleep(5) + except Exception as e: + self.log.info( + f"Couldn't check graph from {ln.name} because {e}, retrying in 5 seconds..." + ) + sleep(5) + + ch_ann_threads = [ + threading.Thread(target=ln_all_chs, args=(self, ln)) for ln in self.lns.values() + ] + for thread in ch_ann_threads: + sleep(0.25) + thread.start() + + all(thread.join() is None for thread in ch_ann_threads) + self.log.info("All LN nodes have complete graph") + + ## + # UPDATE CHANNEL POLICIES + ## + self.log.info("Updating channel policies...") + + def update_policy(self, ln, txid_hex, policy, capacity): + self.log.info(f"Sending update from {ln.name} for channel with outpoint: {txid_hex}:0") + res = None + while res is None: + try: + res = ln.update(txid_hex, policy, capacity) + if len(res["failed_updates"]) != 0: + self.log.info( + f" Failed updates: {res['failed_updates']}\n txid: {txid_hex}\n policy:{policy}\n retrying in 5 seconds..." + ) + sleep(5) + continue + break + except Exception as e: + self.log.info( + f"Couldn't update channel policy for {ln.name} because {e}, retrying in 5 seconds..." + ) + sleep(5) + + update_threads = [] + for ch in self.channels: + if "source_policy" in ch: + ts = threading.Thread( + target=update_policy, + args=( + self, + self.lns[ch["source"]], + ch["txid"], + ch["source_policy"], + ch["capacity"], + ), + ) + sleep(0.25) + ts.start() + update_threads.append(ts) + if "target_policy" in ch: + tt = threading.Thread( + target=update_policy, + args=( + self, + self.lns[ch["target"]], + ch["txid"], + ch["target_policy"], + ch["capacity"], + ), + ) + sleep(0.25) + tt.start() + update_threads.append(tt) + count = len(update_threads) + + all(thread.join() is None for thread in update_threads) + self.log.info(f"Sent {count} channel policy updates") + + self.log.info("Waiting for all channel policy gossip to synchronize...") + + def policy_equal(pol1, pol2, capacity): + return pol1.to_lnd_chanpolicy(capacity) == pol2.to_lnd_chanpolicy(capacity) + + def matching_graph(self, expected, ln): + done = False + while not done: + try: + actual = ln.graph()["edges"] + except Exception as e: + self.log.info( + f"Couldn't get graph from {ln.name} because {e}, retrying in 5 seconds..." + ) + sleep(5) + continue + + self.log.debug(f"LN {ln.name} channel graph edges: {actual}") + if len(actual) > 0: + done = True + assert len(expected) == len(actual), ( + f"Expected edges {len(expected)}, actual edges {len(actual)}\n{actual}" + ) + for i, actual_ch in enumerate(actual): + expected_ch = expected[i] + capacity = expected_ch["capacity"] + # We assert this because it isn't updated as part of policy. + # If this fails we have a bigger issue + assert int(actual_ch["capacity"]) == capacity, ( + f"LN {ln.name} graph capacity mismatch:\n actual: {actual_ch['capacity']}\n expected: {capacity}" + ) + + # Policies were not defined in network.yaml + if "source_policy" not in expected_ch or "target_policy" not in expected_ch: + continue + + # policy actual/expected source/target + polas = Policy.from_lnd_describegraph(actual_ch["node1_policy"]) + polat = Policy.from_lnd_describegraph(actual_ch["node2_policy"]) + poles = Policy(**expected_ch["source_policy"]) + polet = Policy(**expected_ch["target_policy"]) + # Allow policy swap when comparing channels + if policy_equal(polas, poles, capacity) and policy_equal( + polat, polet, capacity + ): + continue + if policy_equal(polas, polet, capacity) and policy_equal( + polat, poles, capacity + ): + continue + done = False + if done: + self.log.info(f"LN {ln.name} graph channel policies all match expected source") + else: + sleep(5) + + expected = sorted(self.channels, key=lambda ch: (ch["id"]["block"], ch["id"]["index"])) + policy_threads = [ + threading.Thread(target=matching_graph, args=(self, expected, ln)) + for ln in self.lns.values() + ] + for thread in policy_threads: + sleep(0.25) + thread.start() + + all(thread.join() is None for thread in policy_threads) + self.log.info("All LN nodes have matching graph!") + + +def main(): + LNInit("").main() + + +if __name__ == "__main__": + main() diff --git a/src/warnet/scenarios/miner_std.py b/resources/scenarios/miner_std.py similarity index 67% rename from src/warnet/scenarios/miner_std.py rename to resources/scenarios/miner_std.py index 063e07f5e..d39f83913 100755 --- a/src/warnet/scenarios/miner_std.py +++ b/resources/scenarios/miner_std.py @@ -2,29 +2,26 @@ from time import sleep -from warnet.scenarios.utils import ensure_miner -from warnet.test_framework_bridge import WarnetTestFramework - - -def cli_help(): - return "Generate blocks over time. Options: [--allnodes | --interval= | --mature ]" +from commander import Commander class Miner: def __init__(self, node, mature): self.node = node - self.wallet = ensure_miner(self.node) + self.wallet = Commander.ensure_miner(self.node) self.addr = self.wallet.getnewaddress() self.mature = mature -class MinerStd(WarnetTestFramework): +class MinerStd(Commander): def set_test_params(self): # This is just a minimum self.num_nodes = 0 self.miners = [] def add_options(self, parser): + parser.description = "Generate blocks over time" + parser.usage = "warnet run /path/to/miner_std.py [options]" parser.add_argument( "--allnodes", dest="allnodes", @@ -44,18 +41,21 @@ def add_options(self, parser): action="store_true", help="When true, generate 101 blocks ONCE per miner", ) + parser.add_argument( + "--tank", + dest="tank", + type=str, + help="Select one tank by name as the only miner", + ) def run_test(self): - while not self.warnet.network_connected(): - self.log.info("Waiting for complete network connection...") - sleep(5) - self.log.info("Network connected. Starting miners.") - - max_miners = 1 - if self.options.allnodes: - max_miners = len(self.nodes) - for index in range(max_miners): - self.miners.append(Miner(self.nodes[index], self.options.mature)) + self.log.info("Starting miners.") + if self.options.tank: + self.miners = [Miner(self.tanks[self.options.tank], self.options.mature)] + else: + max_miners = len(self.nodes) if self.options.allnodes else 1 + for index in range(max_miners): + self.miners.append(Miner(self.nodes[index], self.options.mature)) while True: for miner in self.miners: @@ -74,5 +74,9 @@ def run_test(self): sleep(self.options.interval) +def main(): + MinerStd("").main() + + if __name__ == "__main__": - MinerStd().main() + main() diff --git a/resources/scenarios/reconnaissance.py b/resources/scenarios/reconnaissance.py new file mode 100755 index 000000000..895e3cf94 --- /dev/null +++ b/resources/scenarios/reconnaissance.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +import socket + +from commander import Commander + +# The entire Bitcoin Core test_framework directory is available as a library +from test_framework.messages import MSG_TX, CInv, hash256, msg_getdata +from test_framework.p2p import MAGIC_BYTES, P2PInterface + + +def get_signet_network_magic_from_node(node): + template = node.getblocktemplate({"rules": ["segwit", "signet"]}) + challenge = template["signet_challenge"] + challenge_bytes = bytes.fromhex(challenge) + data = len(challenge_bytes).to_bytes() + challenge_bytes + digest = hash256(data) + return digest[0:4] + + +# The actual scenario is a class like a Bitcoin Core functional test. +# Commander is a subclass of BitcoinTestFramework instide Warnet +# that allows to operate on containerized nodes instead of local nodes. +class Reconnaissance(Commander): + def set_test_params(self): + # This setting is ignored but still required as + # a sub-class of BitcoinTestFramework + self.num_nodes = 1 + + def add_options(self, parser): + parser.description = "Demonstrate network reconnaissance using a scenario and P2PInterface" + parser.usage = "warnet run /path/to/reconnaissance.py" + + # Scenario entrypoint + def run_test(self): + self.log.info("Getting peer info") + + # Just like a typical Bitcoin Core functional test, this executes an + # RPC on a node in the network. The actual node at self.nodes[0] may + # be different depending on the user deploying the scenario. Users in + # Warnet may have different namepsace access but everyone should always + # have access to at least one node. + peerinfo = self.nodes[0].getpeerinfo() + for peer in peerinfo: + # You can print out the the scenario logs with `warnet logs` + # which have a list of all this node's peers' addresses and version + self.log.info(f"{peer['addr']} {peer['subver']}") + + # We pick a node on the network to attack + victim = peerinfo[0] + + # regtest or signet + chain = self.nodes[0].chain + + # The victim's address could be an explicit IP address + # OR a kubernetes hostname (use default chain p2p port) + if ":" in victim["addr"]: + dstaddr = victim["addr"].split(":")[0] + else: + dstaddr = socket.gethostbyname(victim["addr"]) + if chain == "regtest": + dstport = 18444 + if chain == "signet": + dstport = 38333 + MAGIC_BYTES["signet"] = get_signet_network_magic_from_node(self.nodes[0]) + + # Now we will use a python-based Bitcoin p2p node to send very specific, + # unusual or non-standard messages to a "victim" node. + self.log.info(f"Attacking {dstaddr}:{dstport}") + attacker = P2PInterface() + attacker.peer_connect( + dstaddr=dstaddr, + dstport=dstport, + net=chain, + timeout_factor=1, + send_version=True, + supports_v2_p2p=False, + )() + attacker.wait_until(lambda: attacker.is_connected, check_connected=False) + + # Send a harmless network message we expect a response to and wait for it + # Ask for TX with a 0 hash + msg = msg_getdata() + msg.inv.append(CInv(t=MSG_TX, h=0)) + attacker.send_and_ping(msg) + attacker.wait_until(lambda: attacker.message_count["notfound"] > 0) + self.log.info(f"Got notfound message from {dstaddr}:{dstport}") + + +def main(): + Reconnaissance("").main() + + +if __name__ == "__main__": + main() diff --git a/resources/scenarios/test_framework/__init__.py b/resources/scenarios/test_framework/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/test_framework/address.py b/resources/scenarios/test_framework/address.py similarity index 92% rename from src/test_framework/address.py rename to resources/scenarios/test_framework/address.py index 5b2e3289a..2c754e35a 100644 --- a/src/test_framework/address.py +++ b/resources/scenarios/test_framework/address.py @@ -47,18 +47,20 @@ class AddressType(enum.Enum): b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' -def create_deterministic_address_bcrt1_p2tr_op_true(): +def create_deterministic_address_bcrt1_p2tr_op_true(explicit_internal_key=None): """ Generates a deterministic bech32m address (segwit v1 output) that can be spent with a witness stack of OP_TRUE and the control block with internal public key (script-path spending). - Returns a tuple with the generated address and the internal key. + Returns a tuple with the generated address and the TaprootInfo object. """ - internal_key = (1).to_bytes(32, 'big') - address = output_key_to_p2tr(taproot_construct(internal_key, [(None, CScript([OP_TRUE]))]).output_pubkey) - assert_equal(address, 'bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka') - return (address, internal_key) + internal_key = explicit_internal_key or (1).to_bytes(32, 'big') + taproot_info = taproot_construct(internal_key, [("only-path", CScript([OP_TRUE]))]) + address = output_key_to_p2tr(taproot_info.output_pubkey) + if explicit_internal_key is None: + assert_equal(address, 'bcrt1p9yfmy5h72durp7zrhlw9lf7jpwjgvwdg0jr0lqmmjtgg83266lqsekaqka') + return (address, taproot_info) def byte_to_base58(b, version): @@ -153,6 +155,9 @@ def output_key_to_p2tr(key, main=False): assert len(key) == 32 return program_to_witness(1, key, main) +def p2a(main=False): + return program_to_witness(1, "4e73", main) + def check_key(key): if (type(key) is str): key = bytes.fromhex(key) # Assuming this is hex string diff --git a/src/test_framework/authproxy.py b/resources/scenarios/test_framework/authproxy.py similarity index 72% rename from src/test_framework/authproxy.py rename to resources/scenarios/test_framework/authproxy.py index 03042877b..9b2fc0f7f 100644 --- a/src/test_framework/authproxy.py +++ b/resources/scenarios/test_framework/authproxy.py @@ -26,7 +26,7 @@ - HTTP connections persist for the life of the AuthServiceProxy object (if server supports HTTP/1.1) -- sends protocol 'version', per JSON-RPC 1.1 +- sends "jsonrpc":"2.0", per JSON-RPC 2.0 - sends proper, incrementing 'id' - sends Basic HTTP authentication headers - parses all JSON numbers that look like floats as Decimal @@ -75,6 +75,7 @@ def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connect self.__service_url = service_url self._service_name = service_name self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests + self.reuse_http_connections = True self.__url = urllib.parse.urlparse(service_url) user = None if self.__url.username is None else self.__url.username.encode('utf8') passwd = None if self.__url.password is None else self.__url.password.encode('utf8') @@ -92,6 +93,8 @@ def __getattr__(self, name): raise AttributeError if self._service_name is not None: name = "%s.%s" % (self._service_name, name) + if not self.reuse_http_connections: + self._set_conn() return AuthServiceProxy(self.__service_url, name, connection=self.__conn) def _request(self, method, path, postdata): @@ -102,47 +105,67 @@ def _request(self, method, path, postdata): 'User-Agent': USER_AGENT, 'Authorization': self.__auth_header, 'Content-type': 'application/json'} + if not self.reuse_http_connections: + self._set_conn() self.__conn.request(method, path, postdata, headers) return self._get_response() + def _json_dumps(self, obj): + return json.dumps(obj, default=serialization_fallback, ensure_ascii=self.ensure_ascii) + def get_request(self, *args, **argsn): AuthServiceProxy.__id_count += 1 - log.debug("-{}-> {} {}".format( + log.debug("-{}-> {} {} {}".format( AuthServiceProxy.__id_count, self._service_name, - json.dumps(args or argsn, default=serialization_fallback, ensure_ascii=self.ensure_ascii), + self._json_dumps(args), + self._json_dumps(argsn), )) + if args and argsn: params = dict(args=args, **argsn) else: params = args or argsn - return {'version': '1.1', + return {'jsonrpc': '2.0', 'method': self._service_name, 'params': params, 'id': AuthServiceProxy.__id_count} def __call__(self, *args, **argsn): - postdata = json.dumps(self.get_request(*args, **argsn), default=serialization_fallback, ensure_ascii=self.ensure_ascii) + postdata = self._json_dumps(self.get_request(*args, **argsn)) response, status = self._request('POST', self.__url.path, postdata.encode('utf-8')) - if response['error'] is not None: - raise JSONRPCException(response['error'], status) - elif 'result' not in response: - raise JSONRPCException({ - 'code': -343, 'message': 'missing JSON-RPC result'}, status) - elif status != HTTPStatus.OK: - raise JSONRPCException({ - 'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status) + # For backwards compatibility tests, accept JSON RPC 1.1 responses + if 'jsonrpc' not in response: + if response['error'] is not None: + raise JSONRPCException(response['error'], status) + elif 'result' not in response: + raise JSONRPCException({ + 'code': -343, 'message': 'missing JSON-RPC result'}, status) + elif status != HTTPStatus.OK: + raise JSONRPCException({ + 'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status) + else: + return response['result'] else: + assert response['jsonrpc'] == '2.0' + if status != HTTPStatus.OK: + raise JSONRPCException({ + 'code': -342, 'message': 'non-200 HTTP status code'}, status) + if 'error' in response: + raise JSONRPCException(response['error'], status) + elif 'result' not in response: + raise JSONRPCException({ + 'code': -343, 'message': 'missing JSON-RPC 2.0 result and error'}, status) return response['result'] def batch(self, rpc_call_list): - postdata = json.dumps(list(rpc_call_list), default=serialization_fallback, ensure_ascii=self.ensure_ascii) + postdata = self._json_dumps(list(rpc_call_list)) log.debug("--> " + postdata) response, status = self._request('POST', self.__url.path, postdata.encode('utf-8')) if status != HTTPStatus.OK: raise JSONRPCException({ - 'code': -342, 'message': 'non-200 HTTP status code but no JSON-RPC error'}, status) + 'code': -342, 'message': 'non-200 HTTP status code'}, status) return response def _get_response(self): @@ -160,17 +183,31 @@ def _get_response(self): raise JSONRPCException({ 'code': -342, 'message': 'missing HTTP response from server'}) + # Check for no-content HTTP status code, which can be returned when an + # RPC client requests a JSON-RPC 2.0 "notification" with no response. + # Currently this is only possible if clients call the _request() method + # directly to send a raw request. + if http_response.status == HTTPStatus.NO_CONTENT: + if len(http_response.read()) != 0: + raise JSONRPCException({'code': -342, 'message': 'Content received with NO CONTENT status code'}) + return None, http_response.status + content_type = http_response.getheader('Content-Type') if content_type != 'application/json': raise JSONRPCException( {'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)}, http_response.status) - responsedata = http_response.read().decode('utf8') + data = http_response.read() + try: + responsedata = data.decode('utf8') + except UnicodeDecodeError as e: + raise JSONRPCException({ + 'code': -342, 'message': f'Cannot decode response in utf8 format, content: {data}, exception: {e}'}) response = json.loads(responsedata, parse_float=decimal.Decimal) elapsed = time.time() - req_start_time if "error" in response and response["error"] is None: - log.debug("<-%s- [%.6f] %s" % (response["id"], elapsed, json.dumps(response["result"], default=serialization_fallback, ensure_ascii=self.ensure_ascii))) + log.debug("<-%s- [%.6f] %s" % (response["id"], elapsed, self._json_dumps(response["result"]))) else: log.debug("<-- [%.6f] %s" % (elapsed, responsedata)) return response, http_response.status diff --git a/src/test_framework/bip340_test_vectors.csv b/resources/scenarios/test_framework/bip340_test_vectors.csv similarity index 75% rename from src/test_framework/bip340_test_vectors.csv rename to resources/scenarios/test_framework/bip340_test_vectors.csv index e068322de..aa317a3b3 100644 --- a/src/test_framework/bip340_test_vectors.csv +++ b/resources/scenarios/test_framework/bip340_test_vectors.csv @@ -14,3 +14,7 @@ index,secret key,public key,aux_rand,message,signature,verification result,comme 12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size 13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order 14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size +15,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,,71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63,TRUE,message of size 0 (added 2022-12) +16,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,11,08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF,TRUE,message of size 1 (added 2022-12) +17,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,0102030405060708090A0B0C0D0E0F1011,5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5,TRUE,message of size 17 (added 2022-12) +18,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999,403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367,TRUE,message of size 100 (added 2022-12) diff --git a/src/test_framework/blockfilter.py b/resources/scenarios/test_framework/blockfilter.py similarity index 94% rename from src/test_framework/blockfilter.py rename to resources/scenarios/test_framework/blockfilter.py index a30e37ea5..a16aa3d34 100644 --- a/src/test_framework/blockfilter.py +++ b/resources/scenarios/test_framework/blockfilter.py @@ -4,7 +4,7 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Helper routines relevant for compact block filters (BIP158). """ -from .siphash import siphash +from .crypto.siphash import siphash def bip158_basic_element_hash(script_pub_key, N, block_hash): @@ -29,7 +29,7 @@ def bip158_basic_element_hash(script_pub_key, N, block_hash): def bip158_relevant_scriptpubkeys(node, block_hash): - """ Determines the basic filter relvant scriptPubKeys as defined in BIP158: + """ Determines the basic filter relevant scriptPubKeys as defined in BIP158: 'A basic filter MUST contain exactly the following items for each transaction in a block: - The previous output script (the script being spent) for each input, except for diff --git a/src/test_framework/blocktools.py b/resources/scenarios/test_framework/blocktools.py similarity index 81% rename from src/test_framework/blocktools.py rename to resources/scenarios/test_framework/blocktools.py index cfd923bab..eb1d3b054 100644 --- a/src/test_framework/blocktools.py +++ b/resources/scenarios/test_framework/blocktools.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2015-2022 The Bitcoin Core developers +# Copyright (c) 2015-present The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Utilities for manipulating blocks and transactions.""" @@ -27,13 +27,15 @@ hash256, ser_uint256, tx_from_hex, - uint256_from_str, + uint256_from_compact, + WITNESS_SCALE_FACTOR, + MAX_SEQUENCE_NONFINAL, ) from .script import ( CScript, CScriptNum, CScriptOp, - OP_1, + OP_0, OP_RETURN, OP_TRUE, ) @@ -45,9 +47,10 @@ ) from .util import assert_equal -WITNESS_SCALE_FACTOR = 4 MAX_BLOCK_SIGOPS = 20000 MAX_BLOCK_SIGOPS_WEIGHT = MAX_BLOCK_SIGOPS * WITNESS_SCALE_FACTOR +MAX_STANDARD_TX_SIGOPS = 4000 +MAX_STANDARD_TX_WEIGHT = 400000 # Genesis block time (regtest) TIME_GENESIS_BLOCK = 1296688602 @@ -64,6 +67,28 @@ VERSIONBITS_LAST_OLD_BLOCK_VERSION = 4 MIN_BLOCKS_TO_KEEP = 288 +REGTEST_RETARGET_PERIOD = 150 + +REGTEST_N_BITS = 0x207fffff # difficulty retargeting is disabled in REGTEST chainparams" +REGTEST_TARGET = 0x7fffff0000000000000000000000000000000000000000000000000000000000 +assert_equal(uint256_from_compact(REGTEST_N_BITS), REGTEST_TARGET) + +DIFF_1_N_BITS = 0x1d00ffff +DIFF_1_TARGET = 0x00000000ffff0000000000000000000000000000000000000000000000000000 +assert_equal(uint256_from_compact(DIFF_1_N_BITS), DIFF_1_TARGET) + +DIFF_4_N_BITS = 0x1c3fffc0 +DIFF_4_TARGET = int(DIFF_1_TARGET / 4) +assert_equal(uint256_from_compact(DIFF_4_N_BITS), DIFF_4_TARGET) + +# From BIP325 +SIGNET_HEADER = b"\xec\xc7\xda\xa2" + +def nbits_str(nbits): + return f"{nbits:08x}" + +def target_str(target): + return f"{target:064x}" def create_block(hashprev=None, coinbase=None, ntime=None, *, version=None, tmpl=None, txlist=None): """Create a block (with regtest difficulty).""" @@ -73,25 +98,24 @@ def create_block(hashprev=None, coinbase=None, ntime=None, *, version=None, tmpl block.nVersion = version or tmpl.get('version') or VERSIONBITS_LAST_OLD_BLOCK_VERSION block.nTime = ntime or tmpl.get('curtime') or int(time.time() + 600) block.hashPrevBlock = hashprev or int(tmpl['previousblockhash'], 0x10) - if tmpl and not tmpl.get('bits') is None: + if tmpl and tmpl.get('bits') is not None: block.nBits = struct.unpack('>I', bytes.fromhex(tmpl['bits']))[0] else: - block.nBits = 0x207fffff # difficulty retargeting is disabled in REGTEST chainparams + block.nBits = REGTEST_N_BITS if coinbase is None: coinbase = create_coinbase(height=tmpl['height']) block.vtx.append(coinbase) if txlist: for tx in txlist: - if not hasattr(tx, 'calc_sha256'): + if type(tx) is str: tx = tx_from_hex(tx) block.vtx.append(tx) block.hashMerkleRoot = block.calc_merkle_root() - block.calc_sha256() return block def get_witness_script(witness_root, witness_nonce): - witness_commitment = uint256_from_str(hash256(ser_uint256(witness_root) + ser_uint256(witness_nonce))) - output_data = WITNESS_COMMITMENT_HEADER + ser_uint256(witness_commitment) + witness_commitment = hash256(ser_uint256(witness_root) + ser_uint256(witness_nonce)) + output_data = WITNESS_COMMITMENT_HEADER + witness_commitment return CScript([OP_RETURN, output_data]) def add_witness_commitment(block, nonce=0): @@ -109,20 +133,18 @@ def add_witness_commitment(block, nonce=0): # witness commitment is the last OP_RETURN output in coinbase block.vtx[0].vout.append(CTxOut(0, get_witness_script(witness_root, witness_nonce))) - block.vtx[0].rehash() block.hashMerkleRoot = block.calc_merkle_root() - block.rehash() def script_BIP34_coinbase_height(height): if height <= 16: res = CScriptOp.encode_op_n(height) - # Append dummy to increase scriptSig size above 2 (see bad-cb-length consensus rule) - return CScript([res, OP_1]) + # Append dummy to increase scriptSig size to 2 (see bad-cb-length consensus rule) + return CScript([res, OP_0]) return CScript([CScriptNum(height)]) -def create_coinbase(height, pubkey=None, *, script_pubkey=None, extra_output_script=None, fees=0, nValue=50): +def create_coinbase(height, pubkey=None, *, script_pubkey=None, extra_output_script=None, fees=0, nValue=50, halving_period=REGTEST_RETARGET_PERIOD): """Create a coinbase transaction. If pubkey is passed in, the coinbase output will be a P2PK output; @@ -131,11 +153,12 @@ def create_coinbase(height, pubkey=None, *, script_pubkey=None, extra_output_scr If extra_output_script is given, make a 0-value output to that script. This is useful to pad block weight/sigops as needed. """ coinbase = CTransaction() - coinbase.vin.append(CTxIn(COutPoint(0, 0xffffffff), script_BIP34_coinbase_height(height), SEQUENCE_FINAL)) + coinbase.nLockTime = height - 1 + coinbase.vin.append(CTxIn(COutPoint(0, 0xffffffff), script_BIP34_coinbase_height(height), MAX_SEQUENCE_NONFINAL)) coinbaseoutput = CTxOut() coinbaseoutput.nValue = nValue * COIN if nValue == 50: - halvings = int(height / 150) # regtest + halvings = int(height / halving_period) coinbaseoutput.nValue >>= halvings coinbaseoutput.nValue += fees if pubkey is not None: @@ -150,20 +173,20 @@ def create_coinbase(height, pubkey=None, *, script_pubkey=None, extra_output_scr coinbaseoutput2.nValue = 0 coinbaseoutput2.scriptPubKey = extra_output_script coinbase.vout.append(coinbaseoutput2) - coinbase.calc_sha256() return coinbase -def create_tx_with_script(prevtx, n, script_sig=b"", *, amount, script_pub_key=CScript()): +def create_tx_with_script(prevtx, n, script_sig=b"", *, amount, output_script=None): """Return one-input, one-output transaction object spending the prevtx's n-th output with the given amount. Can optionally pass scriptPubKey and scriptSig, default is anyone-can-spend output. """ + if output_script is None: + output_script = CScript() tx = CTransaction() assert n < len(prevtx.vout) - tx.vin.append(CTxIn(COutPoint(prevtx.sha256, n), script_sig, SEQUENCE_FINAL)) - tx.vout.append(CTxOut(amount, script_pub_key)) - tx.calc_sha256() + tx.vin.append(CTxIn(COutPoint(prevtx.txid_int, n), script_sig, SEQUENCE_FINAL)) + tx.vout.append(CTxOut(amount, output_script)) return tx def get_legacy_sigopcount_block(block, accurate=True): diff --git a/resources/scenarios/test_framework/compressor.py b/resources/scenarios/test_framework/compressor.py new file mode 100644 index 000000000..1c30d749d --- /dev/null +++ b/resources/scenarios/test_framework/compressor.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025-present The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Routines for compressing transaction output amounts and scripts.""" +import unittest + +from .messages import COIN + + +def compress_amount(n): + if n == 0: + return 0 + e = 0 + while ((n % 10) == 0) and (e < 9): + n //= 10 + e += 1 + if e < 9: + d = n % 10 + assert (d >= 1 and d <= 9) + n //= 10 + return 1 + (n*9 + d - 1)*10 + e + else: + return 1 + (n - 1)*10 + 9 + + +def decompress_amount(x): + if x == 0: + return 0 + x -= 1 + e = x % 10 + x //= 10 + n = 0 + if e < 9: + d = (x % 9) + 1 + x //= 9 + n = x * 10 + d + else: + n = x + 1 + while e > 0: + n *= 10 + e -= 1 + return n + + +class TestFrameworkCompressor(unittest.TestCase): + def test_amount_compress_decompress(self): + def check_amount(amount, expected_compressed): + self.assertEqual(compress_amount(amount), expected_compressed) + self.assertEqual(decompress_amount(expected_compressed), amount) + + # test cases from compress_tests.cpp:compress_amounts + check_amount(0, 0x0) + check_amount(1, 0x1) + check_amount(1000000, 0x7) + check_amount(COIN, 0x9) + check_amount(50*COIN, 0x32) + check_amount(21000000*COIN, 0x1406f40) diff --git a/src/test_framework/coverage.py b/resources/scenarios/test_framework/coverage.py similarity index 100% rename from src/test_framework/coverage.py rename to resources/scenarios/test_framework/coverage.py diff --git a/resources/scenarios/test_framework/crypto/bip324_cipher.py b/resources/scenarios/test_framework/crypto/bip324_cipher.py new file mode 100644 index 000000000..c9f0fa015 --- /dev/null +++ b/resources/scenarios/test_framework/crypto/bip324_cipher.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Test-only implementation of ChaCha20 Poly1305 AEAD Construction in RFC 8439 and FSChaCha20Poly1305 for BIP 324 + +It is designed for ease of understanding, not performance. + +WARNING: This code is slow and trivially vulnerable to side channel attacks. Do not use for +anything but tests. +""" + +import unittest + +from .chacha20 import chacha20_block, REKEY_INTERVAL +from .poly1305 import Poly1305 + + +def pad16(x): + if len(x) % 16 == 0: + return b'' + return b'\x00' * (16 - (len(x) % 16)) + + +def aead_chacha20_poly1305_encrypt(key, nonce, aad, plaintext): + """Encrypt a plaintext using ChaCha20Poly1305.""" + if plaintext is None: + return None + ret = bytearray() + msg_len = len(plaintext) + for i in range((msg_len + 63) // 64): + now = min(64, msg_len - 64 * i) + keystream = chacha20_block(key, nonce, i + 1) + for j in range(now): + ret.append(plaintext[j + 64 * i] ^ keystream[j]) + poly1305 = Poly1305(chacha20_block(key, nonce, 0)[:32]) + mac_data = aad + pad16(aad) + mac_data += ret + pad16(ret) + mac_data += len(aad).to_bytes(8, 'little') + msg_len.to_bytes(8, 'little') + ret += poly1305.tag(mac_data) + return bytes(ret) + + +def aead_chacha20_poly1305_decrypt(key, nonce, aad, ciphertext): + """Decrypt a ChaCha20Poly1305 ciphertext.""" + if ciphertext is None or len(ciphertext) < 16: + return None + msg_len = len(ciphertext) - 16 + poly1305 = Poly1305(chacha20_block(key, nonce, 0)[:32]) + mac_data = aad + pad16(aad) + mac_data += ciphertext[:-16] + pad16(ciphertext[:-16]) + mac_data += len(aad).to_bytes(8, 'little') + msg_len.to_bytes(8, 'little') + if ciphertext[-16:] != poly1305.tag(mac_data): + return None + ret = bytearray() + for i in range((msg_len + 63) // 64): + now = min(64, msg_len - 64 * i) + keystream = chacha20_block(key, nonce, i + 1) + for j in range(now): + ret.append(ciphertext[j + 64 * i] ^ keystream[j]) + return bytes(ret) + + +class FSChaCha20Poly1305: + """Rekeying wrapper AEAD around ChaCha20Poly1305.""" + def __init__(self, initial_key): + self._key = initial_key + self._packet_counter = 0 + + def _crypt(self, aad, text, is_decrypt): + nonce = ((self._packet_counter % REKEY_INTERVAL).to_bytes(4, 'little') + + (self._packet_counter // REKEY_INTERVAL).to_bytes(8, 'little')) + if is_decrypt: + ret = aead_chacha20_poly1305_decrypt(self._key, nonce, aad, text) + else: + ret = aead_chacha20_poly1305_encrypt(self._key, nonce, aad, text) + if (self._packet_counter + 1) % REKEY_INTERVAL == 0: + rekey_nonce = b"\xFF\xFF\xFF\xFF" + nonce[4:] + self._key = aead_chacha20_poly1305_encrypt(self._key, rekey_nonce, b"", b"\x00" * 32)[:32] + self._packet_counter += 1 + return ret + + def decrypt(self, aad, ciphertext): + return self._crypt(aad, ciphertext, True) + + def encrypt(self, aad, plaintext): + return self._crypt(aad, plaintext, False) + + +# Test vectors from RFC8439 consisting of plaintext, aad, 32 byte key, 12 byte nonce and ciphertext +AEAD_TESTS = [ + # RFC 8439 Example from section 2.8.2 + ["4c616469657320616e642047656e746c656d656e206f662074686520636c6173" + "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" + "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" + "637265656e20776f756c642062652069742e", + "50515253c0c1c2c3c4c5c6c7", + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + [7, 0x4746454443424140], + "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6" + "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36" + "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc" + "3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd060" + "0691"], + # RFC 8439 Test vector A.5 + ["496e7465726e65742d4472616674732061726520647261667420646f63756d65" + "6e74732076616c696420666f722061206d6178696d756d206f6620736978206d" + "6f6e74687320616e64206d617920626520757064617465642c207265706c6163" + "65642c206f72206f62736f6c65746564206279206f7468657220646f63756d65" + "6e747320617420616e792074696d652e20497420697320696e617070726f7072" + "6961746520746f2075736520496e7465726e65742d4472616674732061732072" + "65666572656e6365206d6174657269616c206f7220746f206369746520746865" + "6d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67" + "726573732e2fe2809d", + "f33388860000000000004e91", + "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0", + [0, 0x0807060504030201], + "64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb2" + "4c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf" + "332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c855" + "9797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4" + "b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523e" + "af4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a" + "0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a10" + "49e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29" + "a6ad5cb4022b02709beead9d67890cbb22392336fea1851f38"], + # Test vectors exercising aad and plaintext which are multiples of 16 bytes. + ["8d2d6a8befd9716fab35819eaac83b33269afb9f1a00fddf66095a6c0cd91951" + "a6b7ad3db580be0674c3f0b55f618e34", + "", + "72ddc73f07101282bbbcf853b9012a9f9695fc5d36b303a97fd0845d0314e0c3", + [0x3432b75f, 0xb3585537eb7f4024], + "f760b8224fb2a317b1b07875092606131232a5b86ae142df5df1c846a7f6341a" + "f2564483dd77f836be45e6230808ffe402a6f0a3e8be074b3d1f4ea8a7b09451"], + ["", + "36970d8a704c065de16250c18033de5a400520ac1b5842b24551e5823a3314f3" + "946285171e04a81ebfbe3566e312e74ab80e94c7dd2ff4e10de0098a58d0f503", + "77adda51d6730b9ad6c995658cbd49f581b2547e7c0c08fcc24ceec797461021", + [0x1f90da88, 0x75dafa3ef84471a4], + "aaae5bb81e8407c94b2ae86ae0c7efbe"], +] + +FSAEAD_TESTS = [ + ["d6a4cb04ef0f7c09c1866ed29dc24d820e75b0491032a51b4c3366f9ca35c19e" + "a3047ec6be9d45f9637b63e1cf9eb4c2523a5aab7b851ebeba87199db0e839cf" + "0d5c25e50168306377aedbe9089fd2463ded88b83211cf51b73b150608cc7a60" + "0d0f11b9a742948482e1b109d8faf15b450aa7322e892fa2208c6691e3fecf4c" + "711191b14d75a72147", + "786cb9b6ebf44288974cf0", + "5c9e1c3951a74fba66708bf9d2c217571684556b6a6a3573bff2847d38612654", + 500, + "9dcebbd3281ea3dd8e9a1ef7d55a97abd6743e56ebc0c190cb2c4e14160b385e" + "0bf508dddf754bd02c7c208447c131ce23e47a4a14dfaf5dd8bc601323950f75" + "4e05d46e9232f83fc5120fbbef6f5347a826ec79a93820718d4ec7a2b7cfaaa4" + "4b21e16d726448b62f803811aff4f6d827ed78e738ce8a507b81a8ae13131192" + "8039213de18a5120dc9b7370baca878f50ff254418de3da50c"], + ["8349b7a2690b63d01204800c288ff1138a1d473c832c90ea8b3fc102d0bb3adc" + "44261b247c7c3d6760bfbe979d061c305f46d94c0582ac3099f0bf249f8cb234", + "", + "3bd2093fcbcb0d034d8c569583c5425c1a53171ea299f8cc3bbf9ae3530adfce", + 60000, + "30a6757ff8439b975363f166a0fa0e36722ab35936abd704297948f45083f4d4" + "99433137ce931f7fca28a0acd3bc30f57b550acbc21cbd45bbef0739d9caf30c" + "14b94829deb27f0b1923a2af704ae5d6"], +] + + +class TestFrameworkAEAD(unittest.TestCase): + def test_aead(self): + """ChaCha20Poly1305 AEAD test vectors.""" + for test_vector in AEAD_TESTS: + hex_plain, hex_aad, hex_key, hex_nonce, hex_cipher = test_vector + plain = bytes.fromhex(hex_plain) + aad = bytes.fromhex(hex_aad) + key = bytes.fromhex(hex_key) + nonce = hex_nonce[0].to_bytes(4, 'little') + hex_nonce[1].to_bytes(8, 'little') + + ciphertext = aead_chacha20_poly1305_encrypt(key, nonce, aad, plain) + self.assertEqual(hex_cipher, ciphertext.hex()) + plaintext = aead_chacha20_poly1305_decrypt(key, nonce, aad, ciphertext) + self.assertEqual(plain, plaintext) + + def test_fschacha20poly1305aead(self): + "FSChaCha20Poly1305 AEAD test vectors." + for test_vector in FSAEAD_TESTS: + hex_plain, hex_aad, hex_key, msg_idx, hex_cipher = test_vector + plain = bytes.fromhex(hex_plain) + aad = bytes.fromhex(hex_aad) + key = bytes.fromhex(hex_key) + + enc_aead = FSChaCha20Poly1305(key) + dec_aead = FSChaCha20Poly1305(key) + + for _ in range(msg_idx): + enc_aead.encrypt(b"", None) + ciphertext = enc_aead.encrypt(aad, plain) + self.assertEqual(hex_cipher, ciphertext.hex()) + + for _ in range(msg_idx): + dec_aead.decrypt(b"", None) + plaintext = dec_aead.decrypt(aad, ciphertext) + self.assertEqual(plain, plaintext) diff --git a/resources/scenarios/test_framework/crypto/chacha20.py b/resources/scenarios/test_framework/crypto/chacha20.py new file mode 100644 index 000000000..19b6698df --- /dev/null +++ b/resources/scenarios/test_framework/crypto/chacha20.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Test-only implementation of ChaCha20 cipher and FSChaCha20 for BIP 324 + +It is designed for ease of understanding, not performance. + +WARNING: This code is slow and trivially vulnerable to side channel attacks. Do not use for +anything but tests. +""" + +import unittest + +CHACHA20_INDICES = ( + (0, 4, 8, 12), (1, 5, 9, 13), (2, 6, 10, 14), (3, 7, 11, 15), + (0, 5, 10, 15), (1, 6, 11, 12), (2, 7, 8, 13), (3, 4, 9, 14) +) + +CHACHA20_CONSTANTS = (0x61707865, 0x3320646e, 0x79622d32, 0x6b206574) +REKEY_INTERVAL = 224 # packets + + +def rotl32(v, bits): + """Rotate the 32-bit value v left by bits bits.""" + bits %= 32 # Make sure the term below does not throw an exception + return ((v << bits) & 0xffffffff) | (v >> (32 - bits)) + + +def chacha20_doubleround(s): + """Apply a ChaCha20 double round to 16-element state array s. + See https://cr.yp.to/chacha/chacha-20080128.pdf and https://tools.ietf.org/html/rfc8439 + """ + for a, b, c, d in CHACHA20_INDICES: + s[a] = (s[a] + s[b]) & 0xffffffff + s[d] = rotl32(s[d] ^ s[a], 16) + s[c] = (s[c] + s[d]) & 0xffffffff + s[b] = rotl32(s[b] ^ s[c], 12) + s[a] = (s[a] + s[b]) & 0xffffffff + s[d] = rotl32(s[d] ^ s[a], 8) + s[c] = (s[c] + s[d]) & 0xffffffff + s[b] = rotl32(s[b] ^ s[c], 7) + + +def chacha20_block(key, nonce, cnt): + """Compute the 64-byte output of the ChaCha20 block function. + Takes as input a 32-byte key, 12-byte nonce, and 32-bit integer counter. + """ + # Initial state. + init = [0] * 16 + init[:4] = CHACHA20_CONSTANTS[:4] + init[4:12] = [int.from_bytes(key[i:i+4], 'little') for i in range(0, 32, 4)] + init[12] = cnt + init[13:16] = [int.from_bytes(nonce[i:i+4], 'little') for i in range(0, 12, 4)] + # Perform 20 rounds. + state = list(init) + for _ in range(10): + chacha20_doubleround(state) + # Add initial values back into state. + for i in range(16): + state[i] = (state[i] + init[i]) & 0xffffffff + # Produce byte output + return b''.join(state[i].to_bytes(4, 'little') for i in range(16)) + +class FSChaCha20: + """Rekeying wrapper stream cipher around ChaCha20.""" + def __init__(self, initial_key, rekey_interval=REKEY_INTERVAL): + self._key = initial_key + self._rekey_interval = rekey_interval + self._block_counter = 0 + self._chunk_counter = 0 + self._keystream = b'' + + def _get_keystream_bytes(self, nbytes): + while len(self._keystream) < nbytes: + nonce = ((0).to_bytes(4, 'little') + (self._chunk_counter // self._rekey_interval).to_bytes(8, 'little')) + self._keystream += chacha20_block(self._key, nonce, self._block_counter) + self._block_counter += 1 + ret = self._keystream[:nbytes] + self._keystream = self._keystream[nbytes:] + return ret + + def crypt(self, chunk): + ks = self._get_keystream_bytes(len(chunk)) + ret = bytes([ks[i] ^ chunk[i] for i in range(len(chunk))]) + if ((self._chunk_counter + 1) % self._rekey_interval) == 0: + self._key = self._get_keystream_bytes(32) + self._block_counter = 0 + self._keystream = b'' + self._chunk_counter += 1 + return ret + + +# Test vectors from RFC7539/8439 consisting of 32 byte key, 12 byte nonce, block counter +# and 64 byte output after applying `chacha20_block` function +CHACHA20_TESTS = [ + ["000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", [0x09000000, 0x4a000000], 1, + "10f1e7e4d13b5915500fdd1fa32071c4c7d1f4c733c068030422aa9ac3d46c4e" + "d2826446079faa0914c2d705d98b02a2b5129cd1de164eb9cbd083e8a2503c4e"], + ["0000000000000000000000000000000000000000000000000000000000000000", [0, 0], 0, + "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7" + "da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586"], + ["0000000000000000000000000000000000000000000000000000000000000000", [0, 0], 1, + "9f07e7be5551387a98ba977c732d080dcb0f29a048e3656912c6533e32ee7aed" + "29b721769ce64e43d57133b074d839d531ed1f28510afb45ace10a1f4b794d6f"], + ["0000000000000000000000000000000000000000000000000000000000000001", [0, 0], 1, + "3aeb5224ecf849929b9d828db1ced4dd832025e8018b8160b82284f3c949aa5a" + "8eca00bbb4a73bdad192b5c42f73f2fd4e273644c8b36125a64addeb006c13a0"], + ["00ff000000000000000000000000000000000000000000000000000000000000", [0, 0], 2, + "72d54dfbf12ec44b362692df94137f328fea8da73990265ec1bbbea1ae9af0ca" + "13b25aa26cb4a648cb9b9d1be65b2c0924a66c54d545ec1b7374f4872e99f096"], + ["0000000000000000000000000000000000000000000000000000000000000000", [0, 0x200000000000000], 0, + "c2c64d378cd536374ae204b9ef933fcd1a8b2288b3dfa49672ab765b54ee27c7" + "8a970e0e955c14f3a88e741b97c286f75f8fc299e8148362fa198a39531bed6d"], + ["000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", [0, 0x4a000000], 1, + "224f51f3401bd9e12fde276fb8631ded8c131f823d2c06e27e4fcaec9ef3cf78" + "8a3b0aa372600a92b57974cded2b9334794cba40c63e34cdea212c4cf07d41b7"], + ["0000000000000000000000000000000000000000000000000000000000000001", [0, 0], 0, + "4540f05a9f1fb296d7736e7b208e3c96eb4fe1834688d2604f450952ed432d41" + "bbe2a0b6ea7566d2a5d1e7e20d42af2c53d792b1c43fea817e9ad275ae546963"], + ["0000000000000000000000000000000000000000000000000000000000000000", [0, 1], 0, + "ef3fdfd6c61578fbf5cf35bd3dd33b8009631634d21e42ac33960bd138e50d32" + "111e4caf237ee53ca8ad6426194a88545ddc497a0b466e7d6bbdb0041b2f586b"], + ["000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", [0, 0x0706050403020100], 0, + "f798a189f195e66982105ffb640bb7757f579da31602fc93ec01ac56f85ac3c1" + "34a4547b733b46413042c9440049176905d3be59ea1c53f15916155c2be8241a"], +] + +FSCHACHA20_TESTS = [ + ["000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "0000000000000000000000000000000000000000000000000000000000000000", 256, + "a93df4ef03011f3db95f60d996e1785df5de38fc39bfcb663a47bb5561928349"], + ["01", "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", 5, "ea"], + ["e93fdb5c762804b9a706816aca31e35b11d2aa3080108ef46a5b1f1508819c0a", + "8ec4c3ccdaea336bdeb245636970be01266509b33f3d2642504eaf412206207a", 4096, + "8bfaa4eacff308fdb4a94a5ff25bd9d0c1f84b77f81239f67ff39d6e1ac280c9"], +] + + +class TestFrameworkChacha(unittest.TestCase): + def test_chacha20(self): + """ChaCha20 test vectors.""" + for test_vector in CHACHA20_TESTS: + hex_key, nonce, counter, hex_output = test_vector + key = bytes.fromhex(hex_key) + nonce_bytes = nonce[0].to_bytes(4, 'little') + nonce[1].to_bytes(8, 'little') + keystream = chacha20_block(key, nonce_bytes, counter) + self.assertEqual(hex_output, keystream.hex()) + + def test_fschacha20(self): + """FSChaCha20 test vectors.""" + for test_vector in FSCHACHA20_TESTS: + hex_plaintext, hex_key, rekey_interval, hex_ciphertext_after_rotation = test_vector + plaintext = bytes.fromhex(hex_plaintext) + key = bytes.fromhex(hex_key) + fsc20 = FSChaCha20(key, rekey_interval) + for _ in range(rekey_interval): + fsc20.crypt(plaintext) + + ciphertext = fsc20.crypt(plaintext) + self.assertEqual(hex_ciphertext_after_rotation, ciphertext.hex()) diff --git a/src/test_framework/ellswift.py b/resources/scenarios/test_framework/crypto/ellswift.py similarity index 99% rename from src/test_framework/ellswift.py rename to resources/scenarios/test_framework/crypto/ellswift.py index 97b10118e..429b7b9f4 100644 --- a/src/test_framework/ellswift.py +++ b/resources/scenarios/test_framework/crypto/ellswift.py @@ -12,7 +12,7 @@ import random import unittest -from test_framework.secp256k1 import FE, G, GE +from test_framework.crypto.secp256k1 import FE, G, GE # Precomputed constant square root of -3 (mod p). MINUS_3_SQRT = FE(-3).sqrt() diff --git a/src/test_framework/ellswift_decode_test_vectors.csv b/resources/scenarios/test_framework/crypto/ellswift_decode_test_vectors.csv similarity index 100% rename from src/test_framework/ellswift_decode_test_vectors.csv rename to resources/scenarios/test_framework/crypto/ellswift_decode_test_vectors.csv diff --git a/resources/scenarios/test_framework/crypto/hkdf.py b/resources/scenarios/test_framework/crypto/hkdf.py new file mode 100644 index 000000000..7e8958733 --- /dev/null +++ b/resources/scenarios/test_framework/crypto/hkdf.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Test-only HKDF-SHA256 implementation + +It is designed for ease of understanding, not performance. + +WARNING: This code is slow and trivially vulnerable to side channel attacks. Do not use for +anything but tests. +""" + +import hashlib +import hmac + + +def hmac_sha256(key, data): + """Compute HMAC-SHA256 from specified byte arrays key and data.""" + return hmac.new(key, data, hashlib.sha256).digest() + + +def hkdf_sha256(length, ikm, salt, info): + """Derive a key using HKDF-SHA256.""" + if len(salt) == 0: + salt = bytes([0] * 32) + prk = hmac_sha256(salt, ikm) + t = b"" + okm = b"" + for i in range((length + 32 - 1) // 32): + t = hmac_sha256(prk, t + info + bytes([i + 1])) + okm += t + return okm[:length] diff --git a/resources/scenarios/test_framework/crypto/muhash.py b/resources/scenarios/test_framework/crypto/muhash.py new file mode 100644 index 000000000..09241f620 --- /dev/null +++ b/resources/scenarios/test_framework/crypto/muhash.py @@ -0,0 +1,55 @@ +# Copyright (c) 2020 Pieter Wuille +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Native Python MuHash3072 implementation.""" + +import hashlib +import unittest + +from .chacha20 import chacha20_block + +def data_to_num3072(data): + """Hash a 32-byte array data to a 3072-bit number using 6 Chacha20 operations.""" + bytes384 = b"" + for counter in range(6): + bytes384 += chacha20_block(data, bytes(12), counter) + return int.from_bytes(bytes384, 'little') + +class MuHash3072: + """Class representing the MuHash3072 computation of a set. + + See https://cseweb.ucsd.edu/~mihir/papers/inchash.pdf and https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-May/014337.html + """ + + MODULUS = 2**3072 - 1103717 + + def __init__(self): + """Initialize for an empty set.""" + self.numerator = 1 + self.denominator = 1 + + def insert(self, data): + """Insert a byte array data in the set.""" + data_hash = hashlib.sha256(data).digest() + self.numerator = (self.numerator * data_to_num3072(data_hash)) % self.MODULUS + + def remove(self, data): + """Remove a byte array from the set.""" + data_hash = hashlib.sha256(data).digest() + self.denominator = (self.denominator * data_to_num3072(data_hash)) % self.MODULUS + + def digest(self): + """Extract the final hash. Does not modify this object.""" + val = (self.numerator * pow(self.denominator, -1, self.MODULUS)) % self.MODULUS + bytes384 = val.to_bytes(384, 'little') + return hashlib.sha256(bytes384).digest() + +class TestFrameworkMuhash(unittest.TestCase): + def test_muhash(self): + muhash = MuHash3072() + muhash.insert(b'\x00' * 32) + muhash.insert((b'\x01' + b'\x00' * 31)) + muhash.remove((b'\x02' + b'\x00' * 31)) + finalized = muhash.digest() + # This mirrors the result in the C++ MuHash3072 unit test + self.assertEqual(finalized[::-1].hex(), "10d312b100cbd32ada024a6646e40d3482fcff103668d2625f10002a607d5863") diff --git a/resources/scenarios/test_framework/crypto/poly1305.py b/resources/scenarios/test_framework/crypto/poly1305.py new file mode 100644 index 000000000..967b90254 --- /dev/null +++ b/resources/scenarios/test_framework/crypto/poly1305.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Test-only implementation of Poly1305 authenticator + +It is designed for ease of understanding, not performance. + +WARNING: This code is slow and trivially vulnerable to side channel attacks. Do not use for +anything but tests. +""" + +import unittest + + +class Poly1305: + """Class representing a running poly1305 computation.""" + MODULUS = 2**130 - 5 + + def __init__(self, key): + self.r = int.from_bytes(key[:16], 'little') & 0xffffffc0ffffffc0ffffffc0fffffff + self.s = int.from_bytes(key[16:], 'little') + + def tag(self, data): + """Compute the poly1305 tag.""" + acc, length = 0, len(data) + for i in range((length + 15) // 16): + chunk = data[i * 16:min(length, (i + 1) * 16)] + val = int.from_bytes(chunk, 'little') + 256**len(chunk) + acc = (self.r * (acc + val)) % Poly1305.MODULUS + return ((acc + self.s) & 0xffffffffffffffffffffffffffffffff).to_bytes(16, 'little') + + +# Test vectors from RFC7539/8439 consisting of message to be authenticated, 32 byte key and computed 16 byte tag +POLY1305_TESTS = [ + # RFC 7539, section 2.5.2. + ["43727970746f6772617068696320466f72756d2052657365617263682047726f7570", + "85d6be7857556d337f4452fe42d506a80103808afb0db2fd4abff6af4149f51b", + "a8061dc1305136c6c22b8baf0c0127a9"], + # RFC 7539, section A.3. + ["00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + "00000000000000000000000000000000"], + ["416e79207375626d697373696f6e20746f20746865204945544620696e74656e6465642062792074686520436f6e747269627" + "5746f7220666f72207075626c69636174696f6e20617320616c6c206f722070617274206f6620616e204945544620496e7465" + "726e65742d4472616674206f722052464320616e6420616e792073746174656d656e74206d6164652077697468696e2074686" + "520636f6e74657874206f6620616e204945544620616374697669747920697320636f6e7369646572656420616e2022494554" + "4620436f6e747269627574696f6e222e20537563682073746174656d656e747320696e636c756465206f72616c20737461746" + "56d656e747320696e20494554462073657373696f6e732c2061732077656c6c206173207772697474656e20616e6420656c65" + "6374726f6e696320636f6d6d756e69636174696f6e73206d61646520617420616e792074696d65206f7220706c6163652c207" + "768696368206172652061646472657373656420746f", + "0000000000000000000000000000000036e5f6b5c5e06070f0efca96227a863e", + "36e5f6b5c5e06070f0efca96227a863e"], + ["416e79207375626d697373696f6e20746f20746865204945544620696e74656e6465642062792074686520436f6e747269627" + "5746f7220666f72207075626c69636174696f6e20617320616c6c206f722070617274206f6620616e204945544620496e7465" + "726e65742d4472616674206f722052464320616e6420616e792073746174656d656e74206d6164652077697468696e2074686" + "520636f6e74657874206f6620616e204945544620616374697669747920697320636f6e7369646572656420616e2022494554" + "4620436f6e747269627574696f6e222e20537563682073746174656d656e747320696e636c756465206f72616c20737461746" + "56d656e747320696e20494554462073657373696f6e732c2061732077656c6c206173207772697474656e20616e6420656c65" + "6374726f6e696320636f6d6d756e69636174696f6e73206d61646520617420616e792074696d65206f7220706c6163652c207" + "768696368206172652061646472657373656420746f", + "36e5f6b5c5e06070f0efca96227a863e00000000000000000000000000000000", + "f3477e7cd95417af89a6b8794c310cf0"], + ["2754776173206272696c6c69672c20616e642074686520736c6974687920746f7665730a446964206779726520616e6420676" + "96d626c6520696e2074686520776162653a0a416c6c206d696d737920776572652074686520626f726f676f7665732c0a416e" + "6420746865206d6f6d65207261746873206f757467726162652e", + "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0", + "4541669a7eaaee61e708dc7cbcc5eb62"], + ["ffffffffffffffffffffffffffffffff", + "0200000000000000000000000000000000000000000000000000000000000000", + "03000000000000000000000000000000"], + ["02000000000000000000000000000000", + "02000000000000000000000000000000ffffffffffffffffffffffffffffffff", + "03000000000000000000000000000000"], + ["fffffffffffffffffffffffffffffffff0ffffffffffffffffffffffffffffff11000000000000000000000000000000", + "0100000000000000000000000000000000000000000000000000000000000000", + "05000000000000000000000000000000"], + ["fffffffffffffffffffffffffffffffffbfefefefefefefefefefefefefefefe01010101010101010101010101010101", + "0100000000000000000000000000000000000000000000000000000000000000", + "00000000000000000000000000000000"], + ["fdffffffffffffffffffffffffffffff", + "0200000000000000000000000000000000000000000000000000000000000000", + "faffffffffffffffffffffffffffffff"], + ["e33594d7505e43b900000000000000003394d7505e4379cd01000000000000000000000000000000000000000000000001000000000000000000000000000000", + "0100000000000000040000000000000000000000000000000000000000000000", + "14000000000000005500000000000000"], + ["e33594d7505e43b900000000000000003394d7505e4379cd010000000000000000000000000000000000000000000000", + "0100000000000000040000000000000000000000000000000000000000000000", + "13000000000000000000000000000000"], +] + + +class TestFrameworkPoly1305(unittest.TestCase): + def test_poly1305(self): + """Poly1305 test vectors.""" + for test_vector in POLY1305_TESTS: + hex_message, hex_key, hex_tag = test_vector + message = bytes.fromhex(hex_message) + key = bytes.fromhex(hex_key) + tag = bytes.fromhex(hex_tag) + comp_tag = Poly1305(key).tag(message) + self.assertEqual(tag, comp_tag) diff --git a/src/test_framework/ripemd160.py b/resources/scenarios/test_framework/crypto/ripemd160.py similarity index 100% rename from src/test_framework/ripemd160.py rename to resources/scenarios/test_framework/crypto/ripemd160.py diff --git a/src/test_framework/secp256k1.py b/resources/scenarios/test_framework/crypto/secp256k1.py similarity index 96% rename from src/test_framework/secp256k1.py rename to resources/scenarios/test_framework/crypto/secp256k1.py index 2e9e419da..9d85d557a 100644 --- a/src/test_framework/secp256k1.py +++ b/resources/scenarios/test_framework/crypto/secp256k1.py @@ -15,6 +15,9 @@ * G: the secp256k1 generator point """ +import unittest +from hashlib import sha256 +from test_framework.util import assert_not_equal class FE: """Objects of this class represent elements of the field GF(2**256 - 2**32 - 977). @@ -38,7 +41,7 @@ def __init__(self, a=0, b=1): num = (num * b._den) % FE.SIZE else: den = (den * b) % FE.SIZE - assert den != 0 + assert_not_equal(den, 0) if num == 0: den = 1 self._num = num @@ -344,3 +347,9 @@ def mul(self, a): # Precomputed table with multiples of G for fast multiplication FAST_G = FastGEMul(G) + +class TestFrameworkSecp256k1(unittest.TestCase): + def test_H(self): + H = sha256(G.to_bytes_uncompressed()).digest() + assert GE.lift_x(FE.from_bytes(H)) is not None + self.assertEqual(H.hex(), "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0") diff --git a/src/test_framework/siphash.py b/resources/scenarios/test_framework/crypto/siphash.py similarity index 100% rename from src/test_framework/siphash.py rename to resources/scenarios/test_framework/crypto/siphash.py diff --git a/src/test_framework/xswiftec_inv_test_vectors.csv b/resources/scenarios/test_framework/crypto/xswiftec_inv_test_vectors.csv similarity index 100% rename from src/test_framework/xswiftec_inv_test_vectors.csv rename to resources/scenarios/test_framework/crypto/xswiftec_inv_test_vectors.csv diff --git a/src/test_framework/descriptors.py b/resources/scenarios/test_framework/descriptors.py similarity index 100% rename from src/test_framework/descriptors.py rename to resources/scenarios/test_framework/descriptors.py diff --git a/src/test_framework/key.py b/resources/scenarios/test_framework/key.py similarity index 97% rename from src/test_framework/key.py rename to resources/scenarios/test_framework/key.py index 6c1892539..558dcbf23 100644 --- a/src/test_framework/key.py +++ b/resources/scenarios/test_framework/key.py @@ -13,7 +13,8 @@ import random import unittest -from test_framework import secp256k1 +from test_framework.crypto import secp256k1 +from test_framework.util import assert_not_equal, random_bitflip # Point with no known discrete log. H_POINT = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" @@ -241,10 +242,9 @@ def verify_schnorr(key, sig, msg): - key is a 32-byte xonly pubkey (computed using compute_xonly_pubkey). - sig is a 64-byte Schnorr signature - - msg is a 32-byte message + - msg is a variable-length message """ assert len(key) == 32 - assert len(msg) == 32 assert len(sig) == 64 P = secp256k1.GE.from_bytes_xonly(key) @@ -271,7 +271,6 @@ def sign_schnorr(key, msg, aux=None, flip_p=False, flip_r=False): aux = bytes(32) assert len(key) == 32 - assert len(msg) == 32 assert len(aux) == 32 sec = int.from_bytes(key, 'big') @@ -282,7 +281,7 @@ def sign_schnorr(key, msg, aux=None, flip_p=False, flip_r=False): sec = ORDER - sec t = (sec ^ int.from_bytes(TaggedHash("BIP0340/aux", aux), 'big')).to_bytes(32, 'big') kp = int.from_bytes(TaggedHash("BIP0340/nonce", t + P.to_bytes_xonly() + msg), 'big') % ORDER - assert kp != 0 + assert_not_equal(kp, 0) R = kp * secp256k1.G k = kp if R.y.is_even() != flip_r else ORDER - kp e = int.from_bytes(TaggedHash("BIP0340/challenge", R.to_bytes_xonly() + P.to_bytes_xonly() + msg), 'big') % ORDER @@ -292,11 +291,6 @@ def sign_schnorr(key, msg, aux=None, flip_p=False, flip_r=False): class TestFrameworkKey(unittest.TestCase): def test_ecdsa_and_schnorr(self): """Test the Python ECDSA and Schnorr implementations.""" - def random_bitflip(sig): - sig = list(sig) - sig[random.randrange(len(sig))] ^= (1 << (random.randrange(8))) - return bytes(sig) - byte_arrays = [generate_privkey() for _ in range(3)] + [v.to_bytes(32, 'big') for v in [0, ORDER - 1, ORDER, 2**256 - 1]] keys = {} for privkey_bytes in byte_arrays: # build array of key/pubkey pairs diff --git a/resources/scenarios/test_framework/mempool_util.py b/resources/scenarios/test_framework/mempool_util.py new file mode 100644 index 000000000..3c4609c0b --- /dev/null +++ b/resources/scenarios/test_framework/mempool_util.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# Copyright (c) 2024 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Helpful routines for mempool testing.""" +import random + +from .blocktools import ( + COINBASE_MATURITY, +) +from .messages import ( + COutPoint, + CTransaction, + CTxIn, + CTxInWitness, + CTxOut, +) +from .script import ( + CScript, + OP_RETURN, +) +from .util import ( + assert_equal, + assert_greater_than, + create_lots_of_big_transactions, + gen_return_txouts, +) +from .wallet import ( + MiniWallet, +) + +# Default for -minrelaytxfee in sat/kvB +DEFAULT_MIN_RELAY_TX_FEE = 100 +# Default for -incrementalrelayfee in sat/kvB +DEFAULT_INCREMENTAL_RELAY_FEE = 100 + +TRUC_MAX_VSIZE = 10000 +TRUC_CHILD_MAX_VSIZE = 1000 + +def assert_mempool_contents(test_framework, node, expected=None, sync=True): + """Assert that all transactions in expected are in the mempool, + and no additional ones exist. 'expected' is an array of + CTransaction objects + """ + if sync: + test_framework.sync_mempools() + if not expected: + expected = [] + assert_equal(len(expected), len(set(expected))) + mempool = node.getrawmempool(verbose=False) + assert_equal(len(mempool), len(expected)) + for tx in expected: + assert tx.txid_hex in mempool + + +def fill_mempool(test_framework, node, *, tx_sync_fun=None): + """Fill mempool until eviction. + + Allows for simpler testing of scenarios with floating mempoolminfee > minrelay + Requires -maxmempool=5. + To avoid unintentional tx dependencies, the mempool filling txs are created with a + tagged ephemeral miniwallet instance. + """ + test_framework.log.info("Fill the mempool until eviction is triggered and the mempoolminfee rises") + txouts = gen_return_txouts() + minrelayfee = node.getnetworkinfo()['relayfee'] + + tx_batch_size = 1 + num_of_batches = 75 + # Generate UTXOs to flood the mempool + # 1 to create a tx initially that will be evicted from the mempool later + # 75 transactions each with a fee rate higher than the previous one + ephemeral_miniwallet = MiniWallet(node, tag_name="fill_mempool_ephemeral_wallet") + test_framework.generate(ephemeral_miniwallet, 1 + num_of_batches * tx_batch_size) + + # Mine enough blocks so that the UTXOs are allowed to be spent + test_framework.generate(node, COINBASE_MATURITY - 1) + + # Get all UTXOs up front to ensure none of the transactions spend from each other, as that may + # change their effective feerate and thus the order in which they are selected for eviction. + confirmed_utxos = [ephemeral_miniwallet.get_utxo(confirmed_only=True) for _ in range(num_of_batches * tx_batch_size + 1)] + assert_equal(len(confirmed_utxos), num_of_batches * tx_batch_size + 1) + + test_framework.log.debug("Create a mempool tx that will be evicted") + tx_to_be_evicted_id = ephemeral_miniwallet.send_self_transfer( + from_node=node, utxo_to_spend=confirmed_utxos.pop(0), fee_rate=minrelayfee)["txid"] + + def send_batch(fee): + utxos = confirmed_utxos[:tx_batch_size] + create_lots_of_big_transactions(ephemeral_miniwallet, node, fee, tx_batch_size, txouts, utxos) + del confirmed_utxos[:tx_batch_size] + + # Increase the tx fee rate to give the subsequent transactions a higher priority in the mempool + # The tx has an approx. vsize of 65k, i.e. multiplying the previous fee rate (in sats/kvB) + # by 130 should result in a fee that corresponds to 2x of that fee rate + base_fee = minrelayfee * 130 + batch_fees = [(i + 1) * base_fee for i in range(num_of_batches)] + + test_framework.log.debug("Fill up the mempool with txs with higher fee rate") + for fee in batch_fees[:-3]: + send_batch(fee) + tx_sync_fun() if tx_sync_fun else test_framework.sync_mempools() # sync before any eviction + assert_equal(node.getmempoolinfo()["mempoolminfee"], minrelayfee) + for fee in batch_fees[-3:]: + send_batch(fee) + tx_sync_fun() if tx_sync_fun else test_framework.sync_mempools() # sync after all evictions + + test_framework.log.debug("The tx should be evicted by now") + # The number of transactions created should be greater than the ones present in the mempool + assert_greater_than(tx_batch_size * num_of_batches, len(node.getrawmempool())) + # Initial tx created should not be present in the mempool anymore as it had a lower fee rate + assert tx_to_be_evicted_id not in node.getrawmempool() + + test_framework.log.debug("Check that mempoolminfee is larger than minrelaytxfee") + assert_equal(node.getmempoolinfo()['minrelaytxfee'], minrelayfee) + assert_greater_than(node.getmempoolinfo()['mempoolminfee'], minrelayfee) + +def tx_in_orphanage(node, tx: CTransaction) -> bool: + """Returns true if the transaction is in the orphanage.""" + found = [o for o in node.getorphantxs(verbosity=1) if o["txid"] == tx.txid_hex and o["wtxid"] == tx.wtxid_hex] + return len(found) == 1 + +def create_large_orphan(): + """Create huge orphan transaction""" + tx = CTransaction() + # Nonexistent UTXO + tx.vin = [CTxIn(COutPoint(random.randrange(1 << 256), random.randrange(1, 100)))] + tx.wit.vtxinwit = [CTxInWitness()] + tx.wit.vtxinwit[0].scriptWitness.stack = [CScript(b'X' * 390000)] + tx.vout = [CTxOut(100, CScript([OP_RETURN, b'a' * 20]))] + return tx diff --git a/src/test_framework/messages.py b/resources/scenarios/test_framework/messages.py similarity index 83% rename from src/test_framework/messages.py rename to resources/scenarios/test_framework/messages.py index 8f3aea878..ebb306a82 100755 --- a/src/test_framework/messages.py +++ b/resources/scenarios/test_framework/messages.py @@ -25,15 +25,16 @@ import math import random import socket -import struct import time import unittest -from test_framework.siphash import siphash256 +from test_framework.crypto.siphash import siphash256 from test_framework.util import assert_equal MAX_LOCATOR_SZ = 101 MAX_BLOCK_WEIGHT = 4000000 +DEFAULT_BLOCK_RESERVED_WEIGHT = 8000 +MINIMUM_BLOCK_RESERVED_WEIGHT = 2000 MAX_BLOOM_FILTER_SIZE = 36000 MAX_BLOOM_HASH_FUNCS = 50 @@ -41,12 +42,14 @@ MAX_MONEY = 21000000 * COIN MAX_BIP125_RBF_SEQUENCE = 0xfffffffd # Sequence number that is rbf-opt-in (BIP 125) and csv-opt-out (BIP 68) +MAX_SEQUENCE_NONFINAL = 0xfffffffe # Sequence number that is csv-opt-out (BIP 68) SEQUENCE_FINAL = 0xffffffff # Sequence number that disables nLockTime if set for every input of a tx MAX_PROTOCOL_MESSAGE_LENGTH = 4000000 # Maximum length of incoming protocol messages MAX_HEADERS_RESULTS = 2000 # Number of headers sent in one getheaders result MAX_INV_SIZE = 50000 # Maximum number of entries in an 'inv' protocol message +NODE_NONE = 0 NODE_NETWORK = (1 << 0) NODE_BLOOM = (1 << 2) NODE_WITNESS = (1 << 3) @@ -70,11 +73,23 @@ DEFAULT_ANCESTOR_LIMIT = 25 # default max number of in-mempool ancestors DEFAULT_DESCENDANT_LIMIT = 25 # default max number of in-mempool descendants -# Default setting for -datacarriersize. 80 bytes of data, +1 for OP_RETURN, +2 for the pushdata opcodes. -MAX_OP_RETURN_RELAY = 83 + +# Default setting for -datacarriersize. +MAX_OP_RETURN_RELAY = 100_000 + DEFAULT_MEMPOOL_EXPIRY_HOURS = 336 # hours +TX_MIN_STANDARD_VERSION = 1 +TX_MAX_STANDARD_VERSION = 3 + +MAGIC_BYTES = { + "mainnet": b"\xf9\xbe\xb4\xd9", + "testnet4": b"\x1c\x16\x3f\x28", + "regtest": b"\xfa\xbf\xb5\xda", + "signet": b"\x0a\x03\xcf\x40", +} + def sha256(s): return hashlib.sha256(s).digest() @@ -90,27 +105,47 @@ def hash256(s): def ser_compact_size(l): r = b"" if l < 253: - r = struct.pack("B", l) + r = l.to_bytes(1, "little") elif l < 0x10000: - r = struct.pack(" 0 else 0x00)]) + r + if l <= 0x7f: + return r + l = (l >> 7) - 1 + + +def deser_varint(f): + n = 0 + while True: + dat = f.read(1)[0] + n = (n << 7) | (dat & 0x7f) + if (dat & 0x80) > 0: + n += 1 + else: + return n + + def deser_string(f): nit = deser_compact_size(f) return f.read(nit) @@ -198,6 +233,11 @@ def ser_string_vector(l): return r +def deser_block_spent_outputs(f): + nit = deser_compact_size(f) + return [deser_vector(f, CTxOut) for _ in range(nit)] + + def from_hex(obj, hex_string): """Deserialize from a hex string representation (e.g. from RPC) @@ -272,13 +312,13 @@ def deserialize(self, f, *, with_time=True): """Deserialize from addrv1 format (pre-BIP155)""" if with_time: # VERSION messages serialize CAddress objects without time - self.time = struct.unpack("H", f.read(2))[0] + self.port = int.from_bytes(f.read(2), "big") def serialize(self, *, with_time=True): """Serialize in addrv1 format (pre-BIP155)""" @@ -286,20 +326,20 @@ def serialize(self, *, with_time=True): r = b"" if with_time: # VERSION messages serialize CAddress objects without time - r += struct.pack("H", self.port) + r += self.port.to_bytes(2, "big") return r def deserialize_v2(self, f): """Deserialize from addrv2 format (BIP155)""" - self.time = struct.unpack("H", f.read(2))[0] + self.port = int.from_bytes(f.read(2), "big") def serialize_v2(self): """Serialize in addrv2 format (BIP155)""" assert self.net in self.ADDRV2_NET_NAME r = b"" - r += struct.pack("H", self.port) + raise Exception("Address type not supported") + r += self.port.to_bytes(2, "big") return r def __repr__(self): @@ -375,12 +415,12 @@ def __init__(self, t=0, h=0): self.hash = h def deserialize(self, f): - self.type = struct.unpack(" 21000000 * COIN: return False @@ -670,13 +702,13 @@ def get_vsize(self): return math.ceil(self.get_weight() / WITNESS_SCALE_FACTOR) def __repr__(self): - return "CTransaction(nVersion=%i vin=%s vout=%s wit=%s nLockTime=%i)" \ - % (self.nVersion, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime) + return "CTransaction(version=%i vin=%s vout=%s wit=%s nLockTime=%i)" \ + % (self.version, repr(self.vin), repr(self.vout), repr(self.wit), self.nLockTime) class CBlockHeader: - __slots__ = ("hash", "hashMerkleRoot", "hashPrevBlock", "nBits", "nNonce", - "nTime", "nVersion", "sha256") + __slots__ = ("hashMerkleRoot", "hashPrevBlock", "nBits", "nNonce", + "nTime", "nVersion") def __init__(self, header=None): if header is None: @@ -688,9 +720,6 @@ def __init__(self, header=None): self.nTime = header.nTime self.nBits = header.nBits self.nNonce = header.nNonce - self.sha256 = header.sha256 - self.hash = header.hash - self.calc_sha256() def set_null(self): self.nVersion = 4 @@ -699,45 +728,37 @@ def set_null(self): self.nTime = 0 self.nBits = 0 self.nNonce = 0 - self.sha256 = None - self.hash = None def deserialize(self, f): - self.nVersion = struct.unpack(" target: + if self.hash_int > target: return False for tx in self.vtx: if not tx.is_valid(): @@ -809,11 +828,9 @@ def is_valid(self): return True def solve(self): - self.rehash() target = uint256_from_compact(self.nBits) - while self.sha256 > target: + while self.hash_int > target: self.nNonce += 1 - self.rehash() # Calculate the block weight using witness and non-witness # serialization size (does NOT use sigops). @@ -874,12 +891,12 @@ def __init__(self): def deserialize(self, f): self.header.deserialize(f) - self.nonce = struct.unpack(" 0: + self._on_data() + # Socket read methods def data_received(self, t): """asyncio callback when data is read from the socket.""" if len(t) > 0: self.recvbuf += t - self._on_data() + if self.supports_v2_p2p and not self.v2_state.tried_v2_handshake: + self._on_data_v2_handshake() + else: + self._on_data() def _on_data(self): """Try to read P2P messages from the recv buffer. @@ -236,23 +319,48 @@ def _on_data(self): the on_message callback for processing.""" try: while True: - if len(self.recvbuf) < 4: - return - if self.recvbuf[:4] != self.magic_bytes: - raise ValueError("magic bytes mismatch: {} != {}".format(repr(self.magic_bytes), repr(self.recvbuf))) - if len(self.recvbuf) < 4 + 12 + 4 + 4: - return - msgtype = self.recvbuf[4:4+12].split(b"\x00", 1)[0] - msglen = struct.unpack("= MIN_P2P_VERSION_SUPPORTED, "Version {} received. Test framework only supports versions greater than {}".format(message.nVersion, MIN_P2P_VERSION_SUPPORTED) + # for inbound connections, reply to version with own version message + # (could be due to v1 reconnect after a failed v2 handshake) + if not self.p2p_connected_to_node: + self.send_version() + self.reconnect = False if message.nVersion >= 70016 and self.wtxidrelay: - self.send_message(msg_wtxidrelay()) + self.send_without_ping(msg_wtxidrelay()) if self.support_addrv2: - self.send_message(msg_sendaddrv2()) - self.send_message(msg_verack()) + self.send_without_ping(msg_sendaddrv2()) + self.send_without_ping(msg_verack()) self.nServices = message.nServices self.relay = message.relay - self.send_message(msg_getaddr()) + if self.p2p_connected_to_node: + self.send_without_ping(msg_getaddr()) # Connection helper methods - def wait_until(self, test_function_in, *, timeout=60, check_connected=True): + def wait_until(self, test_function_in, *, timeout=60, check_connected=True, check_interval=0.05): def test_function(): if check_connected: assert self.is_connected return test_function_in() - wait_until_helper_internal(test_function, timeout=timeout, lock=p2p_lock, timeout_factor=self.timeout_factor) + wait_until_helper_internal(test_function, timeout=timeout, lock=p2p_lock, timeout_factor=self.timeout_factor, check_interval=check_interval) - def wait_for_connect(self, timeout=60): + def wait_for_connect(self, *, timeout=60): test_function = lambda: self.is_connected self.wait_until(test_function, timeout=timeout, check_connected=False) - def wait_for_disconnect(self, timeout=60): + def wait_for_disconnect(self, *, timeout=60): test_function = lambda: not self.is_connected self.wait_until(test_function, timeout=timeout, check_connected=False) + def wait_for_reconnect(self, *, timeout=60): + def test_function(): + return self.is_connected and self.last_message.get('version') and not self.supports_v2_p2p + self.wait_until(test_function, timeout=timeout, check_connected=False) + # Message receiving helper methods - def wait_for_tx(self, txid, timeout=60): + def wait_for_tx(self, txid, *, timeout=60): def test_function(): if not self.last_message.get('tx'): return False - return self.last_message['tx'].tx.rehash() == txid + return self.last_message['tx'].tx.txid_hex == txid self.wait_until(test_function, timeout=timeout) - def wait_for_block(self, blockhash, timeout=60): + def wait_for_block(self, blockhash, *, timeout=60): def test_function(): - return self.last_message.get("block") and self.last_message["block"].block.rehash() == blockhash + return self.last_message.get("block") and self.last_message["block"].block.hash_int == blockhash self.wait_until(test_function, timeout=timeout) - def wait_for_header(self, blockhash, timeout=60): + def wait_for_header(self, blockhash, *, timeout=60): def test_function(): last_headers = self.last_message.get('headers') if not last_headers: return False - return last_headers.headers[0].rehash() == int(blockhash, 16) + return last_headers.headers[0].hash_int == int(blockhash, 16) self.wait_until(test_function, timeout=timeout) - def wait_for_merkleblock(self, blockhash, timeout=60): + def wait_for_merkleblock(self, blockhash, *, timeout=60): def test_function(): last_filtered_block = self.last_message.get('merkleblock') if not last_filtered_block: return False - return last_filtered_block.merkleblock.header.rehash() == int(blockhash, 16) + return last_filtered_block.merkleblock.header.hash_int == int(blockhash, 16) self.wait_until(test_function, timeout=timeout) - def wait_for_getdata(self, hash_list, timeout=60): + def wait_for_getdata(self, hash_list, *, timeout=60): """Waits for a getdata message. The object hashes in the inventory vector must match the provided hash_list.""" @@ -522,19 +659,21 @@ def test_function(): self.wait_until(test_function, timeout=timeout) - def wait_for_getheaders(self, timeout=60): - """Waits for a getheaders message. + def wait_for_getheaders(self, block_hash=None, *, timeout=60): + """Waits for a getheaders message containing a specific block hash. - Receiving any getheaders message will satisfy the predicate. the last_message["getheaders"] - value must be explicitly cleared before calling this method, or this will return - immediately with success. TODO: change this method to take a hash value and only - return true if the correct block header has been requested.""" + If no block hash is provided, checks whether any getheaders message has been received by the node.""" def test_function(): - return self.last_message.get("getheaders") + last_getheaders = self.last_message.pop("getheaders", None) + if block_hash is None: + return last_getheaders + if last_getheaders is None: + return False + return block_hash == last_getheaders.locator.vHave[0] self.wait_until(test_function, timeout=timeout) - def wait_for_inv(self, expected_inv, timeout=60): + def wait_for_inv(self, expected_inv, *, timeout=60): """Waits for an INV message and checks that the first inv object in the message was as expected.""" if len(expected_inv) > 1: raise NotImplementedError("wait_for_inv() will only verify the first inv object") @@ -546,7 +685,7 @@ def test_function(): self.wait_until(test_function, timeout=timeout) - def wait_for_verack(self, timeout=60): + def wait_for_verack(self, *, timeout=60): def test_function(): return "verack" in self.last_message @@ -554,17 +693,22 @@ def test_function(): # Message sending helper functions - def send_and_ping(self, message, timeout=60): - self.send_message(message) + def send_version(self): + if self.on_connection_send_msg: + self.send_without_ping(self.on_connection_send_msg) + self.on_connection_send_msg = None # Never used again + + def send_and_ping(self, message, *, timeout=60): + self.send_without_ping(message) self.sync_with_ping(timeout=timeout) - def sync_with_ping(self, timeout=60): + def sync_with_ping(self, *, timeout=60): """Ensure ProcessMessages and SendMessages is called on this connection""" # Sending two pings back-to-back, requires that the node calls # `ProcessMessage` twice, and thus ensures `SendMessages` must have # been called at least once - self.send_message(msg_ping(nonce=0)) - self.send_message(msg_ping(nonce=self.ping_counter)) + self.send_without_ping(msg_ping(nonce=0)) + self.send_without_ping(msg_ping(nonce=self.ping_counter)) def test_function(): return self.last_message.get("pong") and self.last_message["pong"].nonce == self.ping_counter @@ -591,7 +735,7 @@ def __init__(self): NetworkThread.listeners = {} NetworkThread.protos = {} - if sys.platform == 'win32': + if platform.system() == 'Windows': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) NetworkThread.network_event_loop = asyncio.new_event_loop() @@ -599,7 +743,7 @@ def run(self): """Start the network thread.""" self.network_event_loop.run_forever() - def close(self, timeout=10): + def close(self, *, timeout=10): """Close the connections and network event loop.""" self.network_event_loop.call_soon_threadsafe(self.network_event_loop.stop) wait_until_helper_internal(lambda: not self.network_event_loop.is_running(), timeout=timeout) @@ -620,6 +764,11 @@ def listen(cls, p2p, callback, port=None, addr=None, idx=1): if addr is None: addr = '127.0.0.1' + def exception_handler(loop, context): + if not p2p.reconnect: + loop.default_exception_handler(context) + + cls.network_event_loop.set_exception_handler(exception_handler) coroutine = cls.create_listen_server(addr, port, callback, p2p) cls.network_event_loop.call_soon_threadsafe(cls.network_event_loop.create_task, coroutine) @@ -633,7 +782,9 @@ def peer_protocol(): protocol function from that dict, and returns it so the event loop can start executing it.""" response = cls.protos.get((addr, port)) - cls.protos[(addr, port)] = None + # remove protocol function from dict only when reconnection doesn't need to happen/already happened + if not proto.reconnect: + cls.protos[(addr, port)] = None return response if (addr, port) not in cls.listeners: @@ -665,13 +816,14 @@ def __init__(self): self.getdata_requests = [] def on_getdata(self, message): - """Check for the tx/block in our stores and if found, reply with an inv message.""" + """Check for the tx/block in our stores and if found, reply with MSG_TX or MSG_BLOCK.""" for inv in message.inv: self.getdata_requests.append(inv.hash) - if (inv.type & MSG_TYPE_MASK) == MSG_TX and inv.hash in self.tx_store.keys(): - self.send_message(msg_tx(self.tx_store[inv.hash])) - elif (inv.type & MSG_TYPE_MASK) == MSG_BLOCK and inv.hash in self.block_store.keys(): - self.send_message(msg_block(self.block_store[inv.hash])) + invtype = inv.type & MSG_TYPE_MASK + if (invtype == MSG_TX or invtype == MSG_WTX) and inv.hash in self.tx_store.keys(): + self.send_without_ping(msg_tx(self.tx_store[inv.hash])) + elif invtype == MSG_BLOCK and inv.hash in self.block_store.keys(): + self.send_without_ping(msg_block(self.block_store[inv.hash])) else: logger.debug('getdata message type {} received.'.format(hex(inv.type))) @@ -685,14 +837,14 @@ def on_getheaders(self, message): return headers_list = [self.block_store[self.last_block_hash]] - while headers_list[-1].sha256 not in locator.vHave: + while headers_list[-1].hash_int not in locator.vHave: # Walk back through the block store, adding headers to headers_list # as we go. prev_block_hash = headers_list[-1].hashPrevBlock if prev_block_hash in self.block_store: prev_block_header = CBlockHeader(self.block_store[prev_block_hash]) headers_list.append(prev_block_header) - if prev_block_header.sha256 == hash_stop: + if prev_block_header.hash_int == hash_stop: # if this is the hashstop header, stop here break else: @@ -704,9 +856,9 @@ def on_getheaders(self, message): response = msg_headers(headers_list) if response is not None: - self.send_message(response) + self.send_without_ping(response) - def send_blocks_and_test(self, blocks, node, *, success=True, force_send=False, reject_reason=None, expect_disconnect=False, timeout=60): + def send_blocks_and_test(self, blocks, node, *, success=True, force_send=False, reject_reason=None, expect_disconnect=False, timeout=60, is_decoy=False): """Send blocks to test node and test whether the tip advances. - add all blocks to our block_store @@ -720,18 +872,20 @@ def send_blocks_and_test(self, blocks, node, *, success=True, force_send=False, with p2p_lock: for block in blocks: - self.block_store[block.sha256] = block - self.last_block_hash = block.sha256 + self.block_store[block.hash_int] = block + self.last_block_hash = block.hash_int reject_reason = [reject_reason] if reject_reason else [] with node.assert_debug_log(expected_msgs=reject_reason): + if is_decoy: # since decoy messages are ignored by the recipient - no need to wait for response + force_send = True if force_send: for b in blocks: - self.send_message(msg_block(block=b)) + self.send_without_ping(msg_block(block=b), is_decoy) else: - self.send_message(msg_headers([CBlockHeader(block) for block in blocks])) + self.send_without_ping(msg_headers([CBlockHeader(block) for block in blocks])) self.wait_until( - lambda: blocks[-1].sha256 in self.getdata_requests, + lambda: blocks[-1].hash_int in self.getdata_requests, timeout=timeout, check_connected=success, ) @@ -742,47 +896,43 @@ def send_blocks_and_test(self, blocks, node, *, success=True, force_send=False, self.sync_with_ping(timeout=timeout) if success: - self.wait_until(lambda: node.getbestblockhash() == blocks[-1].hash, timeout=timeout) + self.wait_until(lambda: node.getbestblockhash() == blocks[-1].hash_hex, timeout=timeout) else: - assert node.getbestblockhash() != blocks[-1].hash + assert_not_equal(node.getbestblockhash(), blocks[-1].hash_hex) - def send_txs_and_test(self, txs, node, *, success=True, expect_disconnect=False, reject_reason=None): + def send_txs_and_test(self, txs, node, *, success=True, reject_reason=None): """Send txs to test node and test whether they're accepted to the mempool. - add all txs to our tx_store - send tx messages for all txs - if success is True/False: assert that the txs are/are not accepted to the mempool - - if expect_disconnect is True: Skip the sync with ping - if reject_reason is set: assert that the correct reject message is logged.""" with p2p_lock: for tx in txs: - self.tx_store[tx.sha256] = tx + self.tx_store[tx.txid_int] = tx reject_reason = [reject_reason] if reject_reason else [] with node.assert_debug_log(expected_msgs=reject_reason): for tx in txs: - self.send_message(msg_tx(tx)) + self.send_without_ping(msg_tx(tx)) - if expect_disconnect: - self.wait_for_disconnect() - else: - self.sync_with_ping() + self.sync_with_ping() raw_mempool = node.getrawmempool() if success: # Check that all txs are now in the mempool for tx in txs: - assert tx.hash in raw_mempool, "{} not found in mempool".format(tx.hash) + assert tx.txid_hex in raw_mempool, "{} not found in mempool".format(tx.txid_hex) else: # Check that none of the txs are now in the mempool for tx in txs: - assert tx.hash not in raw_mempool, "{} tx found in mempool".format(tx.hash) + assert tx.txid_hex not in raw_mempool, "{} tx found in mempool".format(tx.txid_hex) class P2PTxInvStore(P2PInterface): """A P2PInterface which stores a count of how many times each txid has been announced.""" - def __init__(self): - super().__init__() + def __init__(self, **kwargs): + super().__init__(**kwargs) self.tx_invs_received = defaultdict(int) def on_inv(self, message): @@ -797,7 +947,7 @@ def get_invs(self): with p2p_lock: return list(self.tx_invs_received.keys()) - def wait_for_broadcast(self, txns, timeout=60): + def wait_for_broadcast(self, txns, *, timeout=60): """Waits for the txns (list of txids) to complete initial broadcast. The mempool should mark unbroadcast=False for these transactions. """ diff --git a/src/test_framework/psbt.py b/resources/scenarios/test_framework/psbt.py similarity index 92% rename from src/test_framework/psbt.py rename to resources/scenarios/test_framework/psbt.py index 1eff4a250..4fe688ec0 100644 --- a/src/test_framework/psbt.py +++ b/resources/scenarios/test_framework/psbt.py @@ -50,6 +50,9 @@ PSBT_IN_TAP_BIP32_DERIVATION = 0x16 PSBT_IN_TAP_INTERNAL_KEY = 0x17 PSBT_IN_TAP_MERKLE_ROOT = 0x18 +PSBT_IN_MUSIG2_PARTICIPANT_PUBKEYS = 0x1a +PSBT_IN_MUSIG2_PUB_NONCE = 0x1b +PSBT_IN_MUSIG2_PARTIAL_SIG = 0x1c PSBT_IN_PROPRIETARY = 0xfc # per-output types @@ -61,6 +64,7 @@ PSBT_OUT_TAP_INTERNAL_KEY = 0x05 PSBT_OUT_TAP_TREE = 0x06 PSBT_OUT_TAP_BIP32_DERIVATION = 0x07 +PSBT_OUT_MUSIG2_PARTICIPANT_PUBKEYS = 0x08 PSBT_OUT_PROPRIETARY = 0xfc @@ -88,6 +92,9 @@ def serialize(self): for k,v in self.map.items(): if isinstance(k, int) and 0 <= k and k <= 255: k = bytes([k]) + if isinstance(v, list): + assert all(type(elem) is bytes for elem in v) + v = b"".join(v) # simply concatenate the byte-strings w/o size prefixes m += ser_compact_size(len(k)) + k m += ser_compact_size(len(v)) + v m += b"\x00" diff --git a/src/test_framework/script.py b/resources/scenarios/test_framework/script.py similarity index 91% rename from src/test_framework/script.py rename to resources/scenarios/test_framework/script.py index 17a954cb2..f3cee7c66 100644 --- a/src/test_framework/script.py +++ b/resources/scenarios/test_framework/script.py @@ -8,9 +8,7 @@ """ from collections import namedtuple -import struct import unittest -from typing import List, Dict from .key import TaggedHash, tweak_add_pubkey, compute_xonly_pubkey @@ -19,14 +17,13 @@ CTxOut, hash256, ser_string, - ser_uint256, sha256, - uint256_from_str, ) -from .ripemd160 import ripemd160 +from .crypto.ripemd160 import ripemd160 MAX_SCRIPT_ELEMENT_SIZE = 520 +MAX_SCRIPT_SIZE = 10000 MAX_PUBKEYS_PER_MULTI_A = 999 LOCKTIME_THRESHOLD = 500000000 ANNEX_TAG = 0x50 @@ -59,9 +56,9 @@ def encode_op_pushdata(d): elif len(d) <= 0xff: return b'\x4c' + bytes([len(d)]) + d # OP_PUSHDATA1 elif len(d) <= 0xffff: - return b'\x4d' + struct.pack(b' OP_PUSHDATA4: @@ -591,7 +588,7 @@ def GetSigOpCount(self, fAccurate): n += 1 elif opcode in (OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY): if fAccurate and (OP_1 <= lastOpcode <= OP_16): - n += opcode.decode_op_n() + n += lastOpcode.decode_op_n() else: n += 20 lastOpcode = opcode @@ -671,7 +668,7 @@ def LegacySignatureMsg(script, txTo, inIdx, hashtype): txtmp.vin.append(tmp) s = txtmp.serialize_without_witness() - s += struct.pack(b" 0 or len(program) in [20, 32] + return CScript([version, program]) + + +def script_to_p2wsh_script(script): + script = check_script(script) + return program_to_witness_script(0, sha256(script)) + + +def key_to_p2wpkh_script(key): + key = check_key(key) + return program_to_witness_script(0, hash160(key)) + + +def script_to_p2sh_p2wsh_script(script): + script = check_script(script) + p2shscript = CScript([OP_0, sha256(script)]) + return script_to_p2sh_script(p2shscript) + +def bulk_vout(tx, target_vsize): + if target_vsize < tx.get_vsize(): + raise RuntimeError(f"target_vsize {target_vsize} is less than transaction virtual size {tx.get_vsize()}") + # determine number of needed padding bytes + dummy_vbytes = target_vsize - tx.get_vsize() + # compensate for the increase of the compact-size encoded script length + # (note that the length encoding of the unpadded output script needs one byte) + dummy_vbytes -= len(ser_compact_size(dummy_vbytes)) - 1 + tx.vout[-1].scriptPubKey = CScript([OP_RETURN] + [OP_1] * dummy_vbytes) + assert_equal(tx.get_vsize(), target_vsize) + +def output_key_to_p2tr_script(key): + assert len(key) == 32 + return program_to_witness_script(1, key) + + +def check_key(key): + if isinstance(key, str): + key = bytes.fromhex(key) # Assuming this is hex string + if isinstance(key, bytes) and (len(key) == 33 or len(key) == 65): + return key + assert False + + +def check_script(script): + if isinstance(script, str): + script = bytes.fromhex(script) # Assuming this is hex string + if isinstance(script, bytes) or isinstance(script, CScript): + return script + assert False + + +class ValidWitnessMalleatedTx: + """ + Creates a valid witness malleation transaction test case: + - Parent transaction with a script supporting 2 branches + - 2 child transactions with the same txid but different wtxids + """ + def __init__(self): + hashlock = hash160(b'Preimage') + self.witness_script = CScript([OP_IF, OP_HASH160, hashlock, OP_EQUAL, OP_ELSE, OP_TRUE, OP_ENDIF]) + + def build_parent_tx(self, funding_txid, amount): + # Create an unsigned parent transaction paying to the witness script. + witness_program = sha256(self.witness_script) + script_pubkey = CScript([OP_0, witness_program]) + + parent = CTransaction() + parent.vin.append(CTxIn(COutPoint(int(funding_txid, 16), 0), b"")) + parent.vout.append(CTxOut(int(amount), script_pubkey)) + return parent + + def build_malleated_children(self, signed_parent_txid, amount): + # Create 2 valid children that differ only in witness data. + # 1. Create a new transaction with witness solving first branch + child_witness_script = CScript([OP_TRUE]) + child_witness_program = sha256(child_witness_script) + child_script_pubkey = CScript([OP_0, child_witness_program]) + + child_one = CTransaction() + child_one.vin.append(CTxIn(COutPoint(int(signed_parent_txid, 16), 0), b"")) + child_one.vout.append(CTxOut(int(amount), child_script_pubkey)) + child_one.wit.vtxinwit.append(CTxInWitness()) + child_one.wit.vtxinwit[0].scriptWitness.stack = [b'Preimage', b'\x01', self.witness_script] + + # 2. Create another identical transaction with witness solving second branch + child_two = deepcopy(child_one) + child_two.wit.vtxinwit[0].scriptWitness.stack = [b'', self.witness_script] + return child_one, child_two + + +class TestFrameworkScriptUtil(unittest.TestCase): + def test_multisig(self): + fake_pubkey = bytes([0]*33) + # check correct encoding of P2MS script with n,k <= 16 + normal_ms_script = keys_to_multisig_script([fake_pubkey]*16, k=15) + self.assertEqual(len(normal_ms_script), 1 + 16*34 + 1 + 1) + self.assertTrue(normal_ms_script.startswith(bytes([OP_15]))) + self.assertTrue(normal_ms_script.endswith(bytes([OP_16, OP_CHECKMULTISIG]))) + + # check correct encoding of P2MS script with n,k > 16 + max_ms_script = keys_to_multisig_script([fake_pubkey]*20, k=19) + self.assertEqual(len(max_ms_script), 2 + 20*34 + 2 + 1) + self.assertTrue(max_ms_script.startswith(bytes([1, 19]))) # using OP_PUSH1 + self.assertTrue(max_ms_script.endswith(bytes([1, 20, OP_CHECKMULTISIG]))) diff --git a/src/test_framework/segwit_addr.py b/resources/scenarios/test_framework/segwit_addr.py similarity index 100% rename from src/test_framework/segwit_addr.py rename to resources/scenarios/test_framework/segwit_addr.py diff --git a/src/test_framework/socks5.py b/resources/scenarios/test_framework/socks5.py similarity index 64% rename from src/test_framework/socks5.py rename to resources/scenarios/test_framework/socks5.py index 0ca06a739..0cd16a3ff 100644 --- a/src/test_framework/socks5.py +++ b/resources/scenarios/test_framework/socks5.py @@ -4,11 +4,16 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Dummy Socks5 server for testing.""" +import select import socket import threading import queue import logging +from .netutil import ( + format_addr_port +) + logger = logging.getLogger("TestFramework.socks5") # Protocol constants @@ -32,6 +37,42 @@ def recvall(s, n): n -= len(d) return rv +def sendall(s, data): + """Send all data to a socket, or fail.""" + sent = 0 + while sent < len(data): + _, wlist, _ = select.select([], [s], []) + if len(wlist) > 0: + n = s.send(data[sent:]) + if n == 0: + raise IOError('send() on socket returned 0') + sent += n + +def forward_sockets(a, b): + """Forward data received on socket a to socket b and vice versa, until EOF is received on one of the sockets.""" + # Mark as non-blocking so that we do not end up in a deadlock-like situation + # where we block and wait on data from `a` while there is data ready to be + # received on `b` and forwarded to `a`. And at the same time the application + # at `a` is not sending anything because it waits for the data from `b` to + # respond. + a.setblocking(False) + b.setblocking(False) + sockets = [a, b] + done = False + while not done: + rlist, _, xlist = select.select(sockets, [], sockets) + if len(xlist) > 0: + raise IOError('Exceptional condition on socket') + for s in rlist: + data = s.recv(4096) + if data is None or len(data) == 0: + done = True + break + if s == a: + sendall(b, data) + else: + sendall(a, data) + # Implementation classes class Socks5Configuration(): """Proxy configuration.""" @@ -41,6 +82,19 @@ def __init__(self): self.unauth = False # Support unauthenticated self.auth = False # Support authentication self.keep_alive = False # Do not automatically close connections + # This function is called whenever a new connection arrives to the proxy + # and it decides where the connection is redirected to. It is passed: + # - the address the client requested to connect to + # - the port the client requested to connect to + # It is supposed to return an object like: + # { + # "actual_to_addr": "127.0.0.1" + # "actual_to_port": 28276 + # } + # or None. + # If it returns an object then the connection is redirected to actual_to_addr:actual_to_port. + # If it returns None, or destinations_factory itself is None then the connection is closed. + self.destinations_factory = None class Socks5Command(): """Information about an incoming socks5 command.""" @@ -60,7 +114,7 @@ def __init__(self, serv, conn): self.conn = conn def handle(self): - """Handle socks5 request according to RFC192.""" + """Handle socks5 request according to RFC1928.""" try: # Verify socks version ver = recvall(self.conn, 1)[0] @@ -117,6 +171,22 @@ def handle(self): cmdin = Socks5Command(cmd, atyp, addr, port, username, password) self.serv.queue.put(cmdin) logger.debug('Proxy: %s', cmdin) + + requested_to_addr = addr.decode("utf-8") + requested_to = format_addr_port(requested_to_addr, port) + + if self.serv.conf.destinations_factory is not None: + dest = self.serv.conf.destinations_factory(requested_to_addr, port) + if dest is not None: + logger.debug(f"Serving connection to {requested_to}, will redirect it to " + f"{dest['actual_to_addr']}:{dest['actual_to_port']} instead") + with socket.create_connection((dest["actual_to_addr"], dest["actual_to_port"])) as conn_to: + forward_sockets(self.conn, conn_to) + else: + logger.debug(f"Can't serve the connection to {requested_to}: the destinations factory returned None") + else: + logger.debug(f"Can't serve the connection to {requested_to}: no destinations factory") + # Fall through to disconnect except Exception as e: logger.exception("socks5 request handling failed.") @@ -124,6 +194,8 @@ def handle(self): finally: if not self.serv.keep_alive: self.conn.close() + else: + logger.debug("Keeping client connection alive") class Socks5Server(): def __init__(self, conf): diff --git a/src/test_framework/test_framework.py b/resources/scenarios/test_framework/test_framework.py similarity index 71% rename from src/test_framework/test_framework.py rename to resources/scenarios/test_framework/test_framework.py index 4e6d245b5..0b9295cef 100755 --- a/src/test_framework/test_framework.py +++ b/resources/scenarios/test_framework/test_framework.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2014-2022 The Bitcoin Core developers +# Copyright (c) 2014-present The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Base class for RPC testing.""" @@ -7,19 +7,22 @@ import configparser from enum import Enum import argparse +from datetime import datetime, timezone +import json import logging import os import platform import pdb import random import re +import shlex import shutil import subprocess import sys import tempfile import time +import types -from typing import List from .address import create_deterministic_address_bcrt1_p2tr_op_true from .authproxy import JSONRPCException from . import coverage @@ -30,10 +33,12 @@ PortSeed, assert_equal, check_json_precision, + find_vout_for_address, get_datadir_path, initialize_datadir, p2p_port, wait_until_helper_internal, + wallet_importprivkey, ) @@ -56,6 +61,63 @@ def __init__(self, message): self.message = message +class Binaries: + """Helper class to provide information about bitcoin binaries + + Attributes: + paths: Object returned from get_binary_paths() containing information + which binaries and command lines to use from environment variables and + the config file. + bin_dir: An optional string containing a directory path to look for + binaries, which takes precedence over the paths above, if specified. + This is used by tests calling binaries from previous releases. + """ + def __init__(self, paths, bin_dir): + self.paths = paths + self.bin_dir = bin_dir + + def node_argv(self, **kwargs): + "Return argv array that should be used to invoke bitcoind" + return self._argv("node", self.paths.bitcoind, **kwargs) + + def rpc_argv(self): + "Return argv array that should be used to invoke bitcoin-cli" + # Add -nonamed because "bitcoin rpc" enables -named by default, but bitcoin-cli doesn't + return self._argv("rpc", self.paths.bitcoincli) + ["-nonamed"] + + def tx_argv(self): + "Return argv array that should be used to invoke bitcoin-tx" + return self._argv("tx", self.paths.bitcointx) + + def util_argv(self): + "Return argv array that should be used to invoke bitcoin-util" + return self._argv("util", self.paths.bitcoinutil) + + def wallet_argv(self): + "Return argv array that should be used to invoke bitcoin-wallet" + return self._argv("wallet", self.paths.bitcoinwallet) + + def chainstate_argv(self): + "Return argv array that should be used to invoke bitcoin-chainstate" + return self._argv("chainstate", self.paths.bitcoinchainstate) + + def _argv(self, command, bin_path, need_ipc=False): + """Return argv array that should be used to invoke the command. It + either uses the bitcoin wrapper executable (if BITCOIN_CMD is set or + need_ipc is True), or the direct binary path (bitcoind, etc). When + bin_dir is set (by tests calling binaries from previous releases) it + always uses the direct path.""" + if self.bin_dir is not None: + return [os.path.join(self.bin_dir, os.path.basename(bin_path))] + elif self.paths.bitcoin_cmd is not None or need_ipc: + # If the current test needs IPC functionality, use the bitcoin + # wrapper binary and append -m so it calls multiprocess binaries. + bitcoin_cmd = self.paths.bitcoin_cmd or [self.paths.bitcoin_bin] + return bitcoin_cmd + (["-m"] if need_ipc else []) + [command] + else: + return [bin_path] + + class BitcoinTestMetaClass(type): """Metaclass for BitcoinTestFramework. @@ -92,18 +154,20 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass): This class also contains various public and private helper methods.""" - def __init__(self) -> None: + def __init__(self, test_file) -> None: """Sets test framework defaults. Do not override this method. Instead, override the set_test_params() method""" self.chain: str = 'regtest' self.setup_clean_chain: bool = False - self.nodes: List[TestNode] = [] + self.noban_tx_relay: bool = False + self.nodes: list[TestNode] = [] self.extra_args = None + self.extra_init = None self.network_thread = None self.rpc_timeout = 60 # Wait for up to 60 seconds for the RPC server to respond self.supports_cli = True self.bind_to_localhost_only = True - self.parse_args() - self.default_wallet_name = "default_wallet" if self.options.descriptors else "" + self.parse_args(test_file) + self.default_wallet_name = "default_wallet" self.wallet_data_filename = "wallet.dat" # Optional list of wallet names that can be set in set_test_params to # create and import keys to. If unset, default is len(nodes) * @@ -112,8 +176,9 @@ def __init__(self) -> None: # are not imported. self.wallet_names = None # By default the wallet is not required. Set to true by skip_if_no_wallet(). - # When False, we ignore wallet_names regardless of what it is. - self._requires_wallet = False + # Can also be set to None to indicate that the wallet will be used if available. + # When False or None, we ignore wallet_names in setup_nodes(). + self.uses_wallet = False # Disable ThreadOpenConnections by default, so that adding entries to # addrman will not result in automatic connections to them. self.disable_autoconnect = True @@ -128,42 +193,39 @@ def main(self): try: self.setup() - self.run_test() - except JSONRPCException: - self.log.exception("JSONRPC error") - self.success = TestStatus.FAILED + if self.options.test_methods: + self.run_test_methods() + else: + self.run_test() + except SkipTest as e: self.log.warning("Test Skipped: %s" % e.message) self.success = TestStatus.SKIPPED - except AssertionError: - self.log.exception("Assertion failed") - self.success = TestStatus.FAILED - except KeyError: - self.log.exception("Key error") - self.success = TestStatus.FAILED except subprocess.CalledProcessError as e: - self.log.exception("Called Process failed with '{}'".format(e.output)) + self.log.exception(f"Called Process failed with stdout='{e.stdout}'; stderr='{e.stderr}';") self.success = TestStatus.FAILED - except Exception: - self.log.exception("Unexpected exception caught during testing") - self.success = TestStatus.FAILED - except KeyboardInterrupt: - self.log.warning("Exiting after keyboard interrupt") + except BaseException: + self.log.exception("Unexpected exception") self.success = TestStatus.FAILED finally: exit_code = self.shutdown() sys.exit(exit_code) - def parse_args(self): + def run_test_methods(self): + for method_name in self.options.test_methods: + self.log.info(f"Attempting to execute method: {method_name}") + method = getattr(self, method_name) + method() + self.log.info(f"Method '{method_name}' executed successfully.") + + def parse_args(self, test_file): previous_releases_path = os.getenv("PREVIOUS_RELEASES_DIR") or os.getcwd() + "/releases" parser = argparse.ArgumentParser(usage="%(prog)s [options]") parser.add_argument("--nocleanup", dest="nocleanup", default=False, action="store_true", help="Leave bitcoinds and test.* datadir on exit or error") - parser.add_argument("--noshutdown", dest="noshutdown", default=False, action="store_true", - help="Don't stop bitcoinds after the test execution") - parser.add_argument("--cachedir", dest="cachedir", default=os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/../../cache"), + parser.add_argument("--cachedir", dest="cachedir", default=os.path.abspath(os.path.dirname(test_file) + "/../cache"), help="Directory for caching pregenerated datadirs (default: %(default)s)") - parser.add_argument("--tmpdir", dest="tmpdir", help="Root directory for datadirs") + parser.add_argument("--tmpdir", dest="tmpdir", help="Root directory for datadirs (must not exist)") parser.add_argument("-l", "--loglevel", dest="loglevel", default="INFO", help="log events at this level and higher to the console. Can be set to DEBUG, INFO, WARNING, ERROR or CRITICAL. Passing --loglevel DEBUG will output all logs to console. Note that logs at all levels are always written to the test_framework.log file in the temporary test directory.") parser.add_argument("--tracerpc", dest="trace_rpc", default=False, action="store_true", @@ -176,7 +238,7 @@ def parse_args(self): parser.add_argument("--coveragedir", dest="coveragedir", help="Write tested RPC commands into this directory") parser.add_argument("--configfile", dest="configfile", - default=os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/../../config.ini"), + default=os.path.abspath(os.path.dirname(test_file) + "/../config.ini"), help="Location of the test framework config file (default: %(default)s)") parser.add_argument("--pdbonfailure", dest="pdbonfailure", default=False, action="store_true", help="Attach a python debugger if test fails") @@ -191,6 +253,10 @@ def parse_args(self): parser.add_argument("--timeout-factor", dest="timeout_factor", type=float, help="adjust test timeouts by a factor. Setting it to 0 disables all timeouts") parser.add_argument("--v2transport", dest="v2transport", default=False, action="store_true", help="use BIP324 v2 connections between all nodes by default") + parser.add_argument("--v1transport", dest="v1transport", default=False, action="store_true", + help="Explicitly use v1 transport (can be used to overwrite global --v2transport option)") + parser.add_argument("--test_methods", dest="test_methods", nargs='*', + help="Run specified test methods sequentially instead of the full test. Use only for methods that do not depend on any context set up in run_test or other methods.") self.add_options(parser) # Running TestShell in a Jupyter notebook causes an additional -f argument @@ -199,50 +265,47 @@ def parse_args(self): parser.add_argument("-f", "--fff", help="a dummy argument to fool ipython", default="1") self.options = parser.parse_args() if self.options.timeout_factor == 0: - self.options.timeout_factor = 99999 + self.options.timeout_factor = 999 self.options.timeout_factor = self.options.timeout_factor or (4 if self.options.valgrind else 1) self.options.previous_releases_path = previous_releases_path - config = configparser.ConfigParser() - config.read_file(open(self.options.configfile)) - self.config = config - - if "descriptors" not in self.options: - # Wallet is not required by the test at all and the value of self.options.descriptors won't matter. - # It still needs to exist and be None in order for tests to work however. - # So set it to None to force -disablewallet, because the wallet is not needed. - self.options.descriptors = None - elif self.options.descriptors is None: - # Some wallet is either required or optionally used by the test. - # Prefer SQLite unless it isn't available - if self.is_sqlite_compiled(): - self.options.descriptors = True - elif self.is_bdb_compiled(): - self.options.descriptors = False - else: - # If neither are compiled, tests requiring a wallet will be skipped and the value of self.options.descriptors won't matter - # It still needs to exist and be None in order for tests to work however. - # So set it to None, which will also set -disablewallet. - self.options.descriptors = None + self.config = configparser.ConfigParser() + self.config.read_file(open(self.options.configfile)) + self.binary_paths = self.get_binary_paths() + if self.options.v1transport: + self.options.v2transport=False PortSeed.n = self.options.port_seed - def set_binary_paths(self): - """Update self.options with the paths of all binaries from environment variables or their default values""" + def get_binary_paths(self): + """Get paths of all binaries from environment variables or their default values""" + paths = types.SimpleNamespace() binaries = { - "bitcoind": ("bitcoind", "BITCOIND"), - "bitcoin-cli": ("bitcoincli", "BITCOINCLI"), - "bitcoin-util": ("bitcoinutil", "BITCOINUTIL"), - "bitcoin-wallet": ("bitcoinwallet", "BITCOINWALLET"), + "bitcoin": "BITCOIN_BIN", + "bitcoind": "BITCOIND", + "bitcoin-cli": "BITCOINCLI", + "bitcoin-util": "BITCOINUTIL", + "bitcoin-tx": "BITCOINTX", + "bitcoin-chainstate": "BITCOINCHAINSTATE", + "bitcoin-wallet": "BITCOINWALLET", } - for binary, [attribute_name, env_variable_name] in binaries.items(): + # Set paths to bitcoin core binaries allowing overrides with environment + # variables. + for binary, env_variable_name in binaries.items(): default_filename = os.path.join( self.config["environment"]["BUILDDIR"], - "src", + "bin", binary + self.config["environment"]["EXEEXT"], ) - setattr(self.options, attribute_name, os.getenv(env_variable_name, default=default_filename)) + setattr(paths, env_variable_name.lower(), os.getenv(env_variable_name, default=default_filename)) + # BITCOIN_CMD environment variable can be specified to invoke bitcoin + # wrapper binary instead of other executables. + paths.bitcoin_cmd = shlex.split(os.getenv("BITCOIN_CMD", "")) or None + return paths + + def get_binaries(self, bin_dir=None): + return Binaries(self.binary_paths, bin_dir) def setup(self): """Call this method to start up the test framework object with options set.""" @@ -251,13 +314,9 @@ def setup(self): self.options.cachedir = os.path.abspath(self.options.cachedir) - config = self.config - - self.set_binary_paths() - os.environ['PATH'] = os.pathsep.join([ - os.path.join(config['environment']['BUILDDIR'], 'src'), - os.path.join(config['environment']['BUILDDIR'], 'src', 'qt'), os.environ['PATH'] + os.path.join(self.config["environment"]["BUILDDIR"], "bin"), + os.environ['PATH'] ]) # Set up temp directory and start logging @@ -307,18 +366,15 @@ def shutdown(self): self.log.debug('Closing down network thread') self.network_thread.close() - if not self.options.noshutdown: + if self.success == TestStatus.FAILED: + self.log.info("Not stopping nodes as test failed. The dangling processes will be cleaned up later.") + else: self.log.info("Stopping nodes") if self.nodes: self.stop_nodes() - else: - for node in self.nodes: - node.cleanup_on_exit = False - self.log.info("Note: bitcoinds were not stopped and may still be running") should_clean_up = ( not self.options.nocleanup and - not self.options.noshutdown and self.success != TestStatus.FAILED and not self.options.perf ) @@ -344,7 +400,7 @@ def shutdown(self): self.log.error("Hint: Call {} '{}' to consolidate all logs".format(os.path.normpath(os.path.dirname(os.path.realpath(__file__)) + "/../combine_logs.py"), self.options.tmpdir)) self.log.error("") self.log.error("If this failure happened unexpectedly or intermittently, please file a bug and provide a link or upload of the combined log.") - self.log.error(self.config['environment']['PACKAGE_BUGREPORT']) + self.log.error(self.config['environment']['CLIENT_BUGREPORT']) self.log.error("") exit_code = TEST_EXIT_FAILED # Logging.shutdown will not remove stream- and filehandlers, so we must @@ -411,7 +467,7 @@ def setup_nodes(self): """Override this method to customize test node setup""" self.add_nodes(self.num_nodes, self.extra_args) self.start_nodes() - if self._requires_wallet: + if self.uses_wallet: self.import_deterministic_coinbase_privkeys() if not self.setup_clean_chain: for n in self.nodes: @@ -436,8 +492,8 @@ def init_wallet(self, *, node): if wallet_name is not False: n = self.nodes[node] if wallet_name is not None: - n.createwallet(wallet_name=wallet_name, descriptors=self.options.descriptors, load_on_startup=True) - n.importprivkey(privkey=n.get_deterministic_priv_key().key, label='coinbase', rescan=True) + n.createwallet(wallet_name=wallet_name, load_on_startup=True) + wallet_importprivkey(n.get_wallet_rpc(wallet_name), n.get_deterministic_priv_key().key, 0, label="coinbase") def run_test(self): """Tests must override this method to define test logic""" @@ -445,29 +501,14 @@ def run_test(self): # Public helper methods. These can be accessed by the subclass test scripts. - def add_wallet_options(self, parser, *, descriptors=True, legacy=True): - kwargs = {} - if descriptors + legacy == 1: - # If only one type can be chosen, set it as default - kwargs["default"] = descriptors - group = parser.add_mutually_exclusive_group( - # If only one type is allowed, require it to be set in test_runner.py - required=os.getenv("REQUIRE_WALLET_TYPE_SET") == "1" and "default" in kwargs) - if descriptors: - group.add_argument("--descriptors", action='store_const', const=True, **kwargs, - help="Run test using a descriptor wallet", dest='descriptors') - if legacy: - group.add_argument("--legacy-wallet", action='store_const', const=False, **kwargs, - help="Run test using legacy wallets", dest='descriptors') - - def add_nodes(self, num_nodes: int, extra_args=None, *, rpchost=None, binary=None, binary_cli=None, versions=None): + def add_nodes(self, num_nodes: int, extra_args=None, *, rpchost=None, versions=None): """Instantiate TestNode objects. Should only be called once after the nodes have been specified in set_test_params().""" - def get_bin_from_version(version, bin_name, bin_default): + def bin_dir_from_version(version): if not version: - return bin_default + return None if version > 219999: # Starting at client version 220000 the first two digits represent # the major version, e.g. v22.0 instead of v0.22.0. @@ -485,7 +526,6 @@ def get_bin_from_version(version, bin_name, bin_default): ), ), 'bin', - bin_name, ) if self.bind_to_localhost_only: @@ -494,30 +534,43 @@ def get_bin_from_version(version, bin_name, bin_default): extra_confs = [[]] * num_nodes if extra_args is None: extra_args = [[]] * num_nodes + # Whitelist peers to speed up tx relay / mempool sync. Don't use it if testing tx relay or timing. + if self.noban_tx_relay: + for i in range(len(extra_args)): + extra_args[i] = extra_args[i] + ["-whitelist=noban,in,out@127.0.0.1"] if versions is None: versions = [None] * num_nodes - if binary is None: - binary = [get_bin_from_version(v, 'bitcoind', self.options.bitcoind) for v in versions] - if binary_cli is None: - binary_cli = [get_bin_from_version(v, 'bitcoin-cli', self.options.bitcoincli) for v in versions] + bin_dirs = [] + for v in versions: + bin_dir = bin_dir_from_version(v) + + # Fail test if any of the needed release binaries is missing + for bin_path in (argv[0] for binaries in (self.get_binaries(bin_dir),) + for argv in (binaries.node_argv(), binaries.rpc_argv())): + + if shutil.which(bin_path) is None: + self.log.error(f"Binary not found: {bin_path}") + if v is None: + raise AssertionError("At least one binary is missing, did you compile?") + raise AssertionError("At least one release binary is missing. " + "Previous releases binaries can be downloaded via `test/get_previous_releases.py`.") + + bin_dirs.append(bin_dir) + + extra_init = [{}] * num_nodes if self.extra_init is None else self.extra_init # type: ignore[var-annotated] + assert_equal(len(extra_init), num_nodes) assert_equal(len(extra_confs), num_nodes) assert_equal(len(extra_args), num_nodes) assert_equal(len(versions), num_nodes) - assert_equal(len(binary), num_nodes) - assert_equal(len(binary_cli), num_nodes) + assert_equal(len(bin_dirs), num_nodes) for i in range(num_nodes): args = list(extra_args[i]) - if self.options.v2transport and ("-v2transport=0" not in args): - args.append("-v2transport=1") - test_node_i = TestNode( - i, - get_datadir_path(self.options.tmpdir, i), + init = dict( chain=self.chain, rpchost=rpchost, timewait=self.rpc_timeout, timeout_factor=self.options.timeout_factor, - bitcoind=binary[i], - bitcoin_cli=binary_cli[i], + binaries=self.get_binaries(bin_dirs[i]), version=versions[i], coverage_dir=self.options.coveragedir, cwd=self.options.tmpdir, @@ -526,8 +579,14 @@ def get_bin_from_version(version, bin_name, bin_default): use_cli=self.options.usecli, start_perf=self.options.perf, use_valgrind=self.options.valgrind, - descriptors=self.options.descriptors, + v2transport=self.options.v2transport, + uses_wallet=self.uses_wallet, ) + init.update(extra_init[i]) + test_node_i = TestNode( + i, + get_datadir_path(self.options.tmpdir, i), + **init) self.nodes.append(test_node_i) if not test_node_i.version_is_at_least(170000): # adjust conf for pre 17 @@ -542,7 +601,7 @@ def start_node(self, i, *args, **kwargs): node.wait_for_rpc_connection() if self.options.coveragedir is not None: - coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc) + coverage.write_all_rpc_commands(self.options.coveragedir, node._rpc) def start_nodes(self, extra_args=None, *args, **kwargs): """Start multiple bitcoinds""" @@ -550,19 +609,14 @@ def start_nodes(self, extra_args=None, *args, **kwargs): if extra_args is None: extra_args = [None] * self.num_nodes assert_equal(len(extra_args), self.num_nodes) - try: - for i, node in enumerate(self.nodes): - node.start(extra_args[i], *args, **kwargs) - for node in self.nodes: - node.wait_for_rpc_connection() - except Exception: - # If one node failed to start, stop the others - self.stop_nodes() - raise + for i, node in enumerate(self.nodes): + node.start(extra_args[i], *args, **kwargs) + for node in self.nodes: + node.wait_for_rpc_connection() if self.options.coveragedir is not None: for node in self.nodes: - coverage.write_all_rpc_commands(self.options.coveragedir, node.rpc) + coverage.write_all_rpc_commands(self.options.coveragedir, node._rpc) def stop_node(self, i, expected_stderr='', wait=0): """Stop a bitcoind test node""" @@ -578,10 +632,16 @@ def stop_nodes(self, wait=0): # Wait for nodes to stop node.wait_until_stopped() - def restart_node(self, i, extra_args=None): + def restart_node(self, i, extra_args=None, clear_addrman=False, *, expected_stderr=''): """Stop and start a test node""" - self.stop_node(i) - self.start_node(i, extra_args) + self.stop_node(i, expected_stderr=expected_stderr) + if clear_addrman: + peers_dat = self.nodes[i].chain_path / "peers.dat" + os.remove(peers_dat) + with self.nodes[i].assert_debug_log(expected_msgs=[f'Creating peers.dat because the file was not found ("{peers_dat}")']): + self.start_node(i, extra_args) + else: + self.start_node(i, extra_args) def wait_for_node_exit(self, i, timeout): self.nodes[i].process.wait(timeout) @@ -596,36 +656,43 @@ def connect_nodes(self, a, b, *, peer_advertises_v2=None, wait_for_connect: bool """ from_connection = self.nodes[a] to_connection = self.nodes[b] - from_num_peers = 1 + len(from_connection.getpeerinfo()) - to_num_peers = 1 + len(to_connection.getpeerinfo()) ip_port = "127.0.0.1:" + str(p2p_port(b)) if peer_advertises_v2 is None: - peer_advertises_v2 = self.options.v2transport + peer_advertises_v2 = from_connection.use_v2transport - if peer_advertises_v2: - from_connection.addnode(node=ip_port, command="onetry", v2transport=True) + if peer_advertises_v2 != from_connection.use_v2transport: + from_connection.addnode(node=ip_port, command="onetry", v2transport=peer_advertises_v2) else: - # skip the optional third argument (default false) for + # skip the optional third argument if it matches the default, for # compatibility with older clients from_connection.addnode(ip_port, "onetry") if not wait_for_connect: return - # poll until version handshake complete to avoid race conditions - # with transaction relaying - # See comments in net_processing: - # * Must have a version message before anything else - # * Must have a verack message before anything else - self.wait_until(lambda: sum(peer['version'] != 0 for peer in from_connection.getpeerinfo()) == from_num_peers) - self.wait_until(lambda: sum(peer['version'] != 0 for peer in to_connection.getpeerinfo()) == to_num_peers) - self.wait_until(lambda: sum(peer['bytesrecv_per_msg'].pop('verack', 0) >= 21 for peer in from_connection.getpeerinfo()) == from_num_peers) - self.wait_until(lambda: sum(peer['bytesrecv_per_msg'].pop('verack', 0) >= 21 for peer in to_connection.getpeerinfo()) == to_num_peers) - # The message bytes are counted before processing the message, so make - # sure it was fully processed by waiting for a ping. - self.wait_until(lambda: sum(peer["bytesrecv_per_msg"].pop("pong", 0) >= 29 for peer in from_connection.getpeerinfo()) == from_num_peers) - self.wait_until(lambda: sum(peer["bytesrecv_per_msg"].pop("pong", 0) >= 29 for peer in to_connection.getpeerinfo()) == to_num_peers) + # Use subversion as peer id. Test nodes have their node number appended to the user agent string + from_connection_subver = from_connection.getnetworkinfo()['subversion'] + to_connection_subver = to_connection.getnetworkinfo()['subversion'] + + def find_conn(node, peer_subversion, inbound): + return next(filter(lambda peer: peer['subver'] == peer_subversion and peer['inbound'] == inbound, node.getpeerinfo()), None) + + self.wait_until(lambda: find_conn(from_connection, to_connection_subver, inbound=False) is not None) + self.wait_until(lambda: find_conn(to_connection, from_connection_subver, inbound=True) is not None) + + def check_bytesrecv(peer, msg_type, min_bytes_recv): + assert peer is not None, "Error: peer disconnected" + return peer['bytesrecv_per_msg'].pop(msg_type, 0) >= min_bytes_recv + + # Poll until version handshake (fSuccessfullyConnected) is complete to + # avoid race conditions, because some message types are blocked from + # being sent or received before fSuccessfullyConnected. + # + # As the flag fSuccessfullyConnected is not exposed, check it by + # waiting for a pong, which can only happen after the flag was set. + self.wait_until(lambda: check_bytesrecv(find_conn(from_connection, to_connection_subver, inbound=False), 'pong', 29)) + self.wait_until(lambda: check_bytesrecv(find_conn(to_connection, from_connection_subver, inbound=True), 'pong', 29)) def disconnect_nodes(self, a, b): def disconnect_nodes_helper(node_a, node_b): @@ -678,25 +745,41 @@ def no_op(self): pass def generate(self, generator, *args, sync_fun=None, **kwargs): - blocks = generator.generate(*args, invalid_call=False, **kwargs) + blocks = generator.generate(*args, called_by_framework=True, **kwargs) sync_fun() if sync_fun else self.sync_all() return blocks def generateblock(self, generator, *args, sync_fun=None, **kwargs): - blocks = generator.generateblock(*args, invalid_call=False, **kwargs) + blocks = generator.generateblock(*args, called_by_framework=True, **kwargs) sync_fun() if sync_fun else self.sync_all() return blocks def generatetoaddress(self, generator, *args, sync_fun=None, **kwargs): - blocks = generator.generatetoaddress(*args, invalid_call=False, **kwargs) + blocks = generator.generatetoaddress(*args, called_by_framework=True, **kwargs) sync_fun() if sync_fun else self.sync_all() return blocks def generatetodescriptor(self, generator, *args, sync_fun=None, **kwargs): - blocks = generator.generatetodescriptor(*args, invalid_call=False, **kwargs) + blocks = generator.generatetodescriptor(*args, called_by_framework=True, **kwargs) sync_fun() if sync_fun else self.sync_all() return blocks + def create_outpoints(self, node, *, outputs): + """Send funds to a given list of `{address: amount}` targets using the bitcoind + wallet and return the corresponding outpoints as a list of dictionaries + `[{"txid": txid, "vout": vout1}, {"txid": txid, "vout": vout2}, ...]`. + The result can be used to specify inputs for RPCs like `createrawtransaction`, + `createpsbt`, `lockunspent` etc.""" + assert all(len(output.keys()) == 1 for output in outputs) + send_res = node.send(outputs) + assert send_res["complete"] + utxos = [] + for output in outputs: + address = list(output.keys())[0] + vout = find_vout_for_address(node, send_res["txid"], address) + utxos.append({"txid": send_res["txid"], "vout": vout}) + return utxos + def sync_blocks(self, nodes=None, wait=1, timeout=60): """ Wait until everybody has the same tip. @@ -746,8 +829,8 @@ def sync_all(self, nodes=None): self.sync_blocks(nodes) self.sync_mempools(nodes) - def wait_until(self, test_function, timeout=60): - return wait_until_helper_internal(test_function, timeout=timeout, timeout_factor=self.options.timeout_factor) + def wait_until(self, test_function, timeout=60, check_interval=0.05): + return wait_until_helper_internal(test_function, timeout=timeout, timeout_factor=self.options.timeout_factor, check_interval=check_interval) # Private helper methods. These should not be accessed by the subclass test scripts. @@ -763,9 +846,16 @@ def _start_logging(self): # User can provide log level as a number or string (eg DEBUG). loglevel was caught as a string, so try to convert it to an int ll = int(self.options.loglevel) if self.options.loglevel.isdigit() else self.options.loglevel.upper() ch.setLevel(ll) + # Format logs the same as bitcoind's debug.log with microprecision (so log files can be concatenated and sorted) - formatter = logging.Formatter(fmt='%(asctime)s.%(msecs)03d000Z %(name)s (%(levelname)s): %(message)s', datefmt='%Y-%m-%dT%H:%M:%S') - formatter.converter = time.gmtime + class MicrosecondFormatter(logging.Formatter): + def formatTime(self, record, _=None): + dt = datetime.fromtimestamp(record.created, timezone.utc) + return dt.strftime('%Y-%m-%dT%H:%M:%S.%f') + + formatter = MicrosecondFormatter( + fmt='%(asctime)sZ %(name)s (%(levelname)s): %(message)s', + ) fh.setFormatter(formatter) ch.setFormatter(formatter) # add the handlers to the logger @@ -799,15 +889,14 @@ def _initialize_chain(self): cache_node_dir, chain=self.chain, extra_conf=["bind=127.0.0.1"], - extra_args=['-disablewallet'], + extra_args=[], rpchost=None, timewait=self.rpc_timeout, timeout_factor=self.options.timeout_factor, - bitcoind=self.options.bitcoind, - bitcoin_cli=self.options.bitcoincli, + binaries=self.get_binaries(), coverage_dir=None, cwd=self.options.tmpdir, - descriptors=self.options.descriptors, + uses_wallet=self.uses_wallet, )) self.start_node(CACHE_NODE_ID) cache_node = self.nodes[CACHE_NODE_ID] @@ -875,6 +964,13 @@ def skip_if_no_py_sqlite3(self): except ImportError: raise SkipTest("sqlite3 module not available.") + def skip_if_no_py_capnp(self): + """Attempt to import the capnp package and skip the test if the import fails.""" + try: + import capnp # type: ignore[import] # noqa: F401 + except ImportError: + raise SkipTest("capnp module not available.") + def skip_if_no_python_bcc(self): """Attempt to import the bcc package and skip the tests if the import fails.""" try: @@ -910,39 +1006,40 @@ def skip_if_no_bitcoind_zmq(self): def skip_if_no_wallet(self): """Skip the running test if wallet has not been compiled.""" - self._requires_wallet = True + self.uses_wallet = True if not self.is_wallet_compiled(): raise SkipTest("wallet has not been compiled.") - if self.options.descriptors: - self.skip_if_no_sqlite() - else: - self.skip_if_no_bdb() - - def skip_if_no_sqlite(self): - """Skip the running test if sqlite has not been compiled.""" - if not self.is_sqlite_compiled(): - raise SkipTest("sqlite has not been compiled.") - - def skip_if_no_bdb(self): - """Skip the running test if BDB has not been compiled.""" - if not self.is_bdb_compiled(): - raise SkipTest("BDB has not been compiled.") def skip_if_no_wallet_tool(self): """Skip the running test if bitcoin-wallet has not been compiled.""" if not self.is_wallet_tool_compiled(): raise SkipTest("bitcoin-wallet has not been compiled") + def skip_if_no_bitcoin_tx(self): + """Skip the running test if bitcoin-tx has not been compiled.""" + if not self.is_bitcoin_tx_compiled(): + raise SkipTest("bitcoin-tx has not been compiled") + def skip_if_no_bitcoin_util(self): """Skip the running test if bitcoin-util has not been compiled.""" if not self.is_bitcoin_util_compiled(): raise SkipTest("bitcoin-util has not been compiled") + def skip_if_no_bitcoin_chainstate(self): + """Skip the running test if bitcoin-chainstate has not been compiled.""" + if not self.is_bitcoin_chainstate_compiled(): + raise SkipTest("bitcoin-chainstate has not been compiled") + def skip_if_no_cli(self): """Skip the running test if bitcoin-cli has not been compiled.""" if not self.is_cli_compiled(): raise SkipTest("bitcoin-cli has not been compiled.") + def skip_if_no_ipc(self): + """Skip the running test if ipc is not compiled.""" + if not self.is_ipc_compiled(): + raise SkipTest("ipc has not been compiled.") + def skip_if_no_previous_releases(self): """Skip the running test if previous releases are not available.""" if not self.has_previous_releases(): @@ -961,6 +1058,11 @@ def skip_if_no_external_signer(self): if not self.is_external_signer_compiled(): raise SkipTest("external signer support has not been compiled.") + def skip_if_running_under_valgrind(self): + """Skip the running test if Valgrind is being used.""" + if self.options.valgrind: + raise SkipTest("This test is not compatible with Valgrind.") + def is_cli_compiled(self): """Checks whether bitcoin-cli was compiled.""" return self.config["components"].getboolean("ENABLE_CLI") @@ -973,22 +1075,22 @@ def is_wallet_compiled(self): """Checks whether the wallet module was compiled.""" return self.config["components"].getboolean("ENABLE_WALLET") - def is_specified_wallet_compiled(self): - """Checks whether wallet support for the specified type - (legacy or descriptor wallet) was compiled.""" - if self.options.descriptors: - return self.is_sqlite_compiled() - else: - return self.is_bdb_compiled() - def is_wallet_tool_compiled(self): """Checks whether bitcoin-wallet was compiled.""" return self.config["components"].getboolean("ENABLE_WALLET_TOOL") + def is_bitcoin_tx_compiled(self): + """Checks whether bitcoin-tx was compiled.""" + return self.config["components"].getboolean("BUILD_BITCOIN_TX") + def is_bitcoin_util_compiled(self): """Checks whether bitcoin-util was compiled.""" return self.config["components"].getboolean("ENABLE_BITCOIN_UTIL") + def is_bitcoin_chainstate_compiled(self): + """Checks whether bitcoin-chainstate was compiled.""" + return self.config["components"].getboolean("ENABLE_BITCOIN_CHAINSTATE") + def is_zmq_compiled(self): """Checks whether the zmq module was compiled.""" return self.config["components"].getboolean("ENABLE_ZMQ") @@ -997,14 +1099,14 @@ def is_usdt_compiled(self): """Checks whether the USDT tracepoints were compiled.""" return self.config["components"].getboolean("ENABLE_USDT_TRACEPOINTS") - def is_sqlite_compiled(self): - """Checks whether the wallet module was compiled with Sqlite support.""" - return self.config["components"].getboolean("USE_SQLITE") - - def is_bdb_compiled(self): - """Checks whether the wallet module was compiled with BDB support.""" - return self.config["components"].getboolean("USE_BDB") + def is_ipc_compiled(self): + """Checks whether ipc was compiled.""" + return self.config["components"].getboolean("ENABLE_IPC") def has_blockfile(self, node, filenum: str): - blocksdir = node.datadir_path / self.chain / 'blocks' - return (blocksdir / f"blk{filenum}.dat").is_file() + return (node.blocks_path/ f"blk{filenum}.dat").is_file() + + def convert_to_json_for_cli(self, text): + if self.options.usecli: + return json.dumps(text) + return text diff --git a/src/test_framework/test_node.py b/resources/scenarios/test_framework/test_node.py similarity index 66% rename from src/test_framework/test_node.py rename to resources/scenarios/test_framework/test_node.py index efbb9001d..1ec2fe8a6 100755 --- a/src/test_framework/test_node.py +++ b/resources/scenarios/test_framework/test_node.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# Copyright (c) 2017-2022 The Bitcoin Core developers +# Copyright (c) 2017-present The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Class for bitcoind node under test""" @@ -8,10 +8,11 @@ import decimal import errno from enum import Enum -import http.client import json import logging import os +import pathlib +import platform import re import subprocess import tempfile @@ -19,6 +20,7 @@ import urllib.parse import collections import shlex +import shutil import sys from pathlib import Path @@ -26,11 +28,12 @@ JSONRPCException, serialization_fallback, ) -from .descriptors import descsum_create -from .p2p import P2P_SUBVERSION +from .messages import NODE_P2P_V2 +from .p2p import P2P_SERVICES, P2P_SUBVERSION from .util import ( MAX_NODES, assert_equal, + assert_not_equal, append_config, delete_cookie_file, get_auth_cookie, @@ -38,9 +41,30 @@ rpc_url, wait_until_helper_internal, p2p_port, + tor_port, ) BITCOIND_PROC_WAIT_TIMEOUT = 60 +# The size of the blocks xor key +# from InitBlocksdirXorKey::xor_key.size() +NUM_XOR_BYTES = 8 +# Many systems have a 128kB limit for a command size. Depending on the +# platform, this limit may be larger or smaller. Moreover, when using the +# 'bitcoin' command, it may internally insert more args, which must be +# accounted for. There is no need to pick the largest possible value here +# anyway and it should be fine to set it to 1kB in tests. +TEST_CLI_MAX_ARG_SIZE = 1024 + +# The null blocks key (all 0s) +NULL_BLK_XOR_KEY = bytes([0] * NUM_XOR_BYTES) +BITCOIN_PID_FILENAME_DEFAULT = "bitcoind.pid" + +if sys.platform.startswith("linux"): + UNIX_PATH_MAX = 108 # includes the trailing NUL +elif sys.platform.startswith(("darwin", "freebsd", "netbsd", "openbsd")): + UNIX_PATH_MAX = 104 +else: # safest portable value + UNIX_PATH_MAX = 92 class FailedToStartError(Exception): @@ -67,7 +91,7 @@ class TestNode(): To make things easier for the test writer, any unrecognised messages will be dispatched to the RPC connection.""" - def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, bitcoind, bitcoin_cli, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, descriptors=False): + def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, binaries, coverage_dir, cwd, extra_conf=None, extra_args=None, use_cli=False, start_perf=False, use_valgrind=False, version=None, v2transport=False, uses_wallet=False, ipcbind=False): """ Kwargs: start_perf (bool): If True, begin profiling the node with `perf` as soon as @@ -83,12 +107,14 @@ def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, self.chain = chain self.rpchost = rpchost self.rpc_timeout = timewait - self.binary = bitcoind + self.binaries = binaries self.coverage_dir = coverage_dir self.cwd = cwd - self.descriptors = descriptors + self.has_explicit_bind = False if extra_conf is not None: append_config(self.datadir_path, extra_conf) + # Remember if there is bind=... in the config file. + self.has_explicit_bind = any(e.startswith("bind=") for e in extra_conf) # Most callers will just need to add extra args to the standard list below. # For those callers that need more flexibility, they can just set the args property directly. # Note that common args are set in the config file (see initialize_datadir) @@ -97,19 +123,29 @@ def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, # Configuration for logging is set as command-line args rather than in the bitcoin.conf file. # This means that starting a bitcoind using the temp dir to debug a failed test won't # spam debug.log. - self.args = [ - self.binary, + self.args = self.binaries.node_argv(need_ipc=ipcbind) + [ f"-datadir={self.datadir_path}", "-logtimemicros", "-debug", "-debugexclude=libevent", "-debugexclude=leveldb", "-debugexclude=rand", - "-uacomment=testnode%d" % i, + "-uacomment=testnode%d" % i, # required for subversion uniqueness across peers ] - if self.descriptors is None: + if uses_wallet is not None and not uses_wallet: self.args.append("-disablewallet") + self.ipc_tmp_dir = None + if ipcbind: + self.ipc_socket_path = self.chain_path / "node.sock" + if len(os.fsencode(self.ipc_socket_path)) < UNIX_PATH_MAX: + self.args.append("-ipcbind=unix") + else: + # Work around default CI path exceeding maximum socket path length. + self.ipc_tmp_dir = pathlib.Path(tempfile.mkdtemp(prefix="test-ipc-")) + self.ipc_socket_path = self.ipc_tmp_dir / "node.sock" + self.args.append(f"-ipcbind=unix:{self.ipc_socket_path}") + # Use valgrind, expect for previous release binaries if use_valgrind and version is None: default_suppressions_file = Path(__file__).parents[3] / "contrib" / "valgrind.supp" @@ -125,19 +161,31 @@ def __init__(self, i, datadir_path, *, chain, rpchost, timewait, timeout_factor, self.args.append("-logsourcelocations") if self.version_is_at_least(239000): self.args.append("-loglevel=trace") + if self.version_is_at_least(299900): + self.args.append("-nologratelimit") + + # Default behavior from global -v2transport flag is added to args to persist it over restarts. + # May be overwritten in individual tests, using extra_args. + self.default_to_v2 = v2transport + if self.version_is_at_least(260000): + # 26.0 and later support v2transport + if v2transport: + self.args.append("-v2transport=1") + else: + self.args.append("-v2transport=0") + # if v2transport is requested via global flag but not supported for node version, ignore it - self.cli = TestNodeCLI(bitcoin_cli, self.datadir_path) + self.cli = TestNodeCLI(binaries, self.datadir_path) self.use_cli = use_cli self.start_perf = start_perf self.running = False self.process = None self.rpc_connected = False - self.rpc = None - self.miniwallet = None + self._rpc = None # Should usually not be accessed directly in tests to allow for --usecli mode + self.reuse_http_connections = True # Must be set before calling get_rpc_proxy() i.e. before restarting node self.url = None self.log = logging.getLogger('TestFramework.node%d' % i) - self.cleanup_on_exit = True # Whether to kill the node when this object goes away # Cache perf subprocesses here by their data output filename. self.perf_subprocesses = {} @@ -179,26 +227,41 @@ def _raise_assertion_error(self, msg: str): def __del__(self): # Ensure that we don't leave any bitcoind processes lying around after # the test ends - if self.process and self.cleanup_on_exit: + if self.process: # Should only happen on test failure # Avoid using logger, as that may have already been shutdown when # this destructor is called. - print(self._node_msg("Cleaning up leftover process")) + print(self._node_msg("Cleaning up leftover process"), file=sys.stderr) self.process.kill() + if self.ipc_tmp_dir: + print(self._node_msg(f"Cleaning up ipc directory {str(self.ipc_tmp_dir)!r}")) + shutil.rmtree(self.ipc_tmp_dir) def __getattr__(self, name): """Dispatches any unrecognised messages to the RPC connection or a CLI instance.""" if self.use_cli: - return getattr(RPCOverloadWrapper(self.cli, True, self.descriptors), name) + return getattr(self.cli, name) else: - assert self.rpc_connected and self.rpc is not None, self._node_msg("Error: no RPC connection") - return getattr(RPCOverloadWrapper(self.rpc, descriptors=self.descriptors), name) + assert self.rpc_connected and self._rpc is not None, self._node_msg("Error: no RPC connection") + return getattr(self._rpc, name) def start(self, extra_args=None, *, cwd=None, stdout=None, stderr=None, env=None, **kwargs): """Start the node.""" if extra_args is None: extra_args = self.extra_args + # If listening and no -bind is given, then bitcoind would bind P2P ports on + # 0.0.0.0:P and 127.0.0.1:P+1 (for incoming Tor connections), where P is + # a unique port chosen by the test framework and configured as port=P in + # bitcoin.conf. To avoid collisions, change it to 127.0.0.1:tor_port(). + will_listen = all(e != "-nolisten" and e != "-listen=0" for e in extra_args) + has_explicit_bind = self.has_explicit_bind or any(e.startswith("-bind=") for e in extra_args) + if will_listen and not has_explicit_bind: + extra_args.append(f"-bind=0.0.0.0:{p2p_port(self.index)}") + extra_args.append(f"-bind=127.0.0.1:{tor_port(self.index)}=onion") + + self.use_v2transport = "-v2transport=1" in extra_args or (self.default_to_v2 and "-v2transport=0" not in extra_args) + # Add a new stdout and stderr file each time bitcoind is started if stderr is None: stderr = tempfile.NamedTemporaryFile(dir=self.stderr_dir, delete=False) @@ -228,10 +291,17 @@ def start(self, extra_args=None, *, cwd=None, stdout=None, stderr=None, env=None if self.start_perf: self._start_perf() - def wait_for_rpc_connection(self): + def wait_for_rpc_connection(self, *, wait_for_import=True): """Sets up an RPC connection to the bitcoind process. Returns False if unable to connect.""" # Poll at a rate of four times per second poll_per_s = 4 + + suppressed_errors = collections.defaultdict(int) + latest_error = None + def suppress_error(category: str, e: Exception): + suppressed_errors[category] += 1 + return (category, repr(e)) + for _ in range(poll_per_s * self.rpc_timeout): if self.process.poll() is not None: # Attach abrupt shutdown error/s to the exception message @@ -248,12 +318,13 @@ def wait_for_rpc_connection(self): timeout=self.rpc_timeout // 2, # Shorter timeout to allow for one retry in case of ETIMEDOUT coveragedir=self.coverage_dir, ) + rpc.auth_service_proxy_instance.reuse_http_connections = self.reuse_http_connections rpc.getblockcount() # If the call to getblockcount() succeeds then the RPC connection is up - if self.version_is_at_least(190000): + if self.version_is_at_least(190000) and wait_for_import: # getmempoolinfo.loaded is available since commit # bb8ae2c (version 0.19.0) - wait_until_helper_internal(lambda: rpc.getmempoolinfo()['loaded'], timeout_factor=self.timeout_factor) + self.wait_until(lambda: rpc.getmempoolinfo()['loaded']) # Wait for the node to finish reindex, block import, and # loading the mempool. Usually importing happens fast or # even "immediate" when the node is started. However, there @@ -272,33 +343,43 @@ def wait_for_rpc_connection(self): # overhead is trivial, and the added guarantees are worth # the minimal performance cost. self.log.debug("RPC successfully started") + # Set rpc_connected even if we are in use_cli mode so that we know we can call self.stop() if needed. + self.rpc_connected = True if self.use_cli: return - self.rpc = rpc - self.rpc_connected = True - self.url = self.rpc.rpc_url + self._rpc = rpc + self.url = self._rpc.rpc_url return - except JSONRPCException as e: # Initialization phase + except JSONRPCException as e: + # Suppress these as they are expected during initialization. # -28 RPC in warmup - # -342 Service unavailable, RPC server started but is shutting down due to error - if e.error['code'] != -28 and e.error['code'] != -342: + # -342 Service unavailable, could be starting up or shutting down + if e.error['code'] not in [-28, -342]: raise # unknown JSON RPC exception - except ConnectionResetError: - # This might happen when the RPC server is in warmup, but shut down before the call to getblockcount - # succeeds. Try again to properly raise the FailedToStartError - pass + latest_error = suppress_error(f"JSONRPCException {e.error['code']}", e) except OSError as e: - if e.errno == errno.ETIMEDOUT: - pass # Treat identical to ConnectionResetError - elif e.errno == errno.ECONNREFUSED: - pass # Port not yet open? - else: + error_num = e.errno + # Work around issue where socket timeouts don't have errno set. + # https://github.com/python/cpython/issues/109601 + if error_num is None and isinstance(e, TimeoutError): + error_num = errno.ETIMEDOUT + + # Suppress similarly to the above JSONRPCException errors. + if error_num not in [ + errno.ECONNRESET, # This might happen when the RPC server is in warmup, + # but shut down before the call to getblockcount succeeds. + errno.ETIMEDOUT, # Treat identical to ECONNRESET + errno.ECONNREFUSED # Port not yet open? + ]: raise # unknown OS error - except ValueError as e: # cookie file not found and no rpcuser or rpcpassword; bitcoind is still starting + latest_error = suppress_error(f"OSError {errno.errorcode[error_num]}", e) + except ValueError as e: + # Suppress if cookie file isn't generated yet and no rpcuser or rpcpassword; bitcoind may be starting. if "No RPC credentials" not in str(e): raise + latest_error = suppress_error("missing_credentials", e) time.sleep(1.0 / poll_per_s) - self._raise_assertion_error("Unable to connect to bitcoind after {}s".format(self.rpc_timeout)) + self._raise_assertion_error(f"Unable to connect to bitcoind after {self.rpc_timeout}s (ignored errors: {dict(suppressed_errors)!s}{'' if latest_error is None else f', latest: {latest_error[0]!r}/{latest_error[1]}'})") def wait_for_cookie_credentials(self): """Ensures auth cookie credentials can be read, e.g. for testing CLI with -rpcwait before RPC connection is up.""" @@ -319,16 +400,16 @@ def generate(self, nblocks, maxtries=1000000, **kwargs): self.log.debug("TestNode.generate() dispatches `generate` call to `generatetoaddress`") return self.generatetoaddress(nblocks=nblocks, address=self.get_deterministic_priv_key().address, maxtries=maxtries, **kwargs) - def generateblock(self, *args, invalid_call, **kwargs): - assert not invalid_call + def generateblock(self, *args, called_by_framework, **kwargs): + assert called_by_framework, "Direct call of this mining RPC is discouraged. Please use one of the self.generate* methods on the test framework, which sync the nodes to avoid intermittent test issues. You may use sync_fun=self.no_op to disable the sync explicitly." return self.__getattr__('generateblock')(*args, **kwargs) - def generatetoaddress(self, *args, invalid_call, **kwargs): - assert not invalid_call + def generatetoaddress(self, *args, called_by_framework, **kwargs): + assert called_by_framework, "Direct call of this mining RPC is discouraged. Please use one of the self.generate* methods on the test framework, which sync the nodes to avoid intermittent test issues. You may use sync_fun=self.no_op to disable the sync explicitly." return self.__getattr__('generatetoaddress')(*args, **kwargs) - def generatetodescriptor(self, *args, invalid_call, **kwargs): - assert not invalid_call + def generatetodescriptor(self, *args, called_by_framework, **kwargs): + assert called_by_framework, "Direct call of this mining RPC is discouraged. Please use one of the self.generate* methods on the test framework, which sync the nodes to avoid intermittent test issues. You may use sync_fun=self.no_op to disable the sync explicitly." return self.__getattr__('generatetodescriptor')(*args, **kwargs) def setmocktime(self, timestamp): @@ -342,11 +423,11 @@ def setmocktime(self, timestamp): def get_wallet_rpc(self, wallet_name): if self.use_cli: - return RPCOverloadWrapper(self.cli("-rpcwallet={}".format(wallet_name)), True, self.descriptors) + return self.cli("-rpcwallet={}".format(wallet_name)) else: - assert self.rpc_connected and self.rpc, self._node_msg("RPC not connected") + assert self.rpc_connected and self._rpc, self._node_msg("RPC not connected") wallet_path = "wallet/{}".format(urllib.parse.quote(wallet_name)) - return RPCOverloadWrapper(self.rpc / wallet_path, descriptors=self.descriptors) + return self._rpc / wallet_path def version_is_at_least(self, ver): return self.version is None or self.version >= ver @@ -355,15 +436,15 @@ def stop_node(self, expected_stderr='', *, wait=0, wait_until_stopped=True): """Stop the node.""" if not self.running: return + assert self.rpc_connected, self._node_msg( + "Should only call stop_node() on a running node after wait_for_rpc_connection() succeeded. " + f"Did you forget to call the latter after start()? Not connected to process: {self.process.pid}") self.log.debug("Stopping node") - try: - # Do not use wait argument when testing older nodes, e.g. in wallet_backwards_compatibility.py - if self.version_is_at_least(180000): - self.stop(wait=wait) - else: - self.stop() - except http.client.CannotSendRequest: - self.log.exception("Unable to stop node.") + # Do not use wait argument when testing older nodes, e.g. in wallet_backwards_compatibility.py + if self.version_is_at_least(180000): + self.stop(wait=wait) + else: + self.stop() # If there are any running perf processes, stop them. for profile_name in tuple(self.perf_subprocesses.keys()): @@ -401,13 +482,19 @@ def is_node_stopped(self, *, expected_stderr="", expected_ret_code=0): self.running = False self.process = None self.rpc_connected = False - self.rpc = None + self._rpc = None self.log.debug("Node stopped") return True def wait_until_stopped(self, *, timeout=BITCOIND_PROC_WAIT_TIMEOUT, expect_error=False, **kwargs): - expected_ret_code = 1 if expect_error else 0 # Whether node shutdown return EXIT_FAILURE or EXIT_SUCCESS - wait_until_helper_internal(lambda: self.is_node_stopped(expected_ret_code=expected_ret_code, **kwargs), timeout=timeout, timeout_factor=self.timeout_factor) + if "expected_ret_code" not in kwargs: + kwargs["expected_ret_code"] = 1 if expect_error else 0 # Whether node shutdown return EXIT_FAILURE or EXIT_SUCCESS + self.wait_until(lambda: self.is_node_stopped(**kwargs), timeout=timeout) + + def kill_process(self): + self.process.kill() + self.wait_until_stopped(expected_ret_code=1 if platform.system() == "Windows" else -9) + assert self.is_node_stopped() def replace_in_config(self, replacements): """ @@ -436,6 +523,14 @@ def debug_log_path(self) -> Path: def blocks_path(self) -> Path: return self.chain_path / "blocks" + @property + def blocks_key_path(self) -> Path: + return self.blocks_path / "xor.dat" + + def read_xor_key(self) -> bytes: + with open(self.blocks_key_path, "rb") as xor_f: + return xor_f.read(NUM_XOR_BYTES) + @property def wallets_path(self) -> Path: return self.chain_path / "wallets" @@ -477,7 +572,7 @@ def assert_debug_log(self, expected_msgs, unexpected_msgs=None, timeout=2): self._raise_assertion_error('Expected messages "{}" does not partially match log:\n\n{}\n\n'.format(str(expected_msgs), print_log)) @contextlib.contextmanager - def wait_for_debug_log(self, expected_msgs, timeout=60): + def busy_wait_for_debug_log(self, expected_msgs, timeout=60): """ Block until we see a particular debug log message fragment or until we exceed the timeout. Return: @@ -512,6 +607,23 @@ def wait_for_debug_log(self, expected_msgs, timeout=60): 'Expected messages "{}" does not partially match log:\n\n{}\n\n'.format( str(expected_msgs), print_log)) + @contextlib.contextmanager + def wait_for_new_peer(self, timeout=5): + """ + Wait until the node is connected to at least one new peer. We detect this + by watching for an increased highest peer id, using the `getpeerinfo` RPC call. + Note that the simpler approach of only accounting for the number of peers + suffers from race conditions, as disconnects from unrelated previous peers + could happen anytime in-between. + """ + def get_highest_peer_id(): + peer_info = self.getpeerinfo() + return peer_info[-1]["id"] if peer_info else -1 + + initial_peer_id = get_highest_peer_id() + yield + self.wait_until(lambda: get_highest_peer_id() > initial_peer_id, timeout=timeout) + @contextlib.contextmanager def profile_with_perf(self, profile_name: str): """ @@ -542,7 +654,7 @@ def test_success(cmd): cmd, shell=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) == 0 - if not sys.platform.startswith('linux'): + if platform.system() != 'Linux': self.log.warning("Can't profile with perf; only available on Linux platforms") return None @@ -605,7 +717,7 @@ def assert_start_raises_init_error(self, extra_args=None, expected_msg=None, mat self.start(extra_args, stdout=log_stdout, stderr=log_stderr, *args, **kwargs) ret = self.process.wait(timeout=self.rpc_timeout) self.log.debug(self._node_msg(f'bitcoind exited with status {ret} during initialization')) - assert ret != 0 # Exit code must indicate failure + assert_not_equal(ret, 0) # Exit code must indicate failure self.running = False self.process = None # Check stderr for expected message @@ -635,19 +747,39 @@ def assert_start_raises_init_error(self, extra_args=None, expected_msg=None, mat assert_msg += "with expected error " + expected_msg self._raise_assertion_error(assert_msg) - def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, **kwargs): + def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, send_version=True, supports_v2_p2p=None, wait_for_v2_handshake=True, expect_success=True, **kwargs): """Add an inbound p2p connection to the node. This method adds the p2p connection to the self.p2ps list and also - returns the connection to the caller.""" + returns the connection to the caller. + + When self.use_v2transport is True, TestNode advertises NODE_P2P_V2 service flag + + An inbound connection is made from TestNode <------ P2PConnection + - if TestNode doesn't advertise NODE_P2P_V2 service, P2PConnection sends version message and v1 P2P is followed + - if TestNode advertises NODE_P2P_V2 service, (and if P2PConnections supports v2 P2P) + P2PConnection sends ellswift bytes and v2 P2P is followed + """ if 'dstport' not in kwargs: kwargs['dstport'] = p2p_port(self.index) if 'dstaddr' not in kwargs: kwargs['dstaddr'] = '127.0.0.1' + if supports_v2_p2p is None: + supports_v2_p2p = self.use_v2transport + + if self.use_v2transport: + kwargs['services'] = kwargs.get('services', P2P_SERVICES) | NODE_P2P_V2 + supports_v2_p2p = self.use_v2transport and supports_v2_p2p + p2p_conn.peer_connect(**kwargs, send_version=send_version, net=self.chain, timeout_factor=self.timeout_factor, supports_v2_p2p=supports_v2_p2p)() - p2p_conn.peer_connect(**kwargs, net=self.chain, timeout_factor=self.timeout_factor)() self.p2ps.append(p2p_conn) + if not expect_success: + return p2p_conn p2p_conn.wait_until(lambda: p2p_conn.is_connected, check_connected=False) + if supports_v2_p2p and wait_for_v2_handshake: + p2p_conn.wait_until(lambda: p2p_conn.v2_state.tried_v2_handshake) + if send_version: + p2p_conn.wait_until(lambda: not p2p_conn.on_connection_send_msg) if wait_for_verack: # Wait for the node to send us the version and verack p2p_conn.wait_for_verack() @@ -674,7 +806,7 @@ def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, **kwargs): return p2p_conn - def add_outbound_p2p_connection(self, p2p_conn, *, wait_for_verack=True, p2p_idx, connection_type="outbound-full-relay", **kwargs): + def add_outbound_p2p_connection(self, p2p_conn, *, wait_for_verack=True, wait_for_disconnect=False, p2p_idx, connection_type="outbound-full-relay", supports_v2_p2p=None, advertise_v2_p2p=None, **kwargs): """Add an outbound p2p connection from node. Must be an "outbound-full-relay", "block-relay-only", "addr-fetch" or "feeler" connection. @@ -684,15 +816,43 @@ def add_outbound_p2p_connection(self, p2p_conn, *, wait_for_verack=True, p2p_idx p2p_idx must be different for simultaneously connected peers. When reusing it for the next peer after disconnecting the previous one, it is necessary to wait for the disconnect to finish to avoid a race condition. + + Parameters: + supports_v2_p2p: whether p2p_conn supports v2 P2P or not + advertise_v2_p2p: whether p2p_conn is advertised to support v2 P2P or not + + An outbound connection is made from TestNode -------> P2PConnection + - if P2PConnection doesn't advertise_v2_p2p, TestNode sends version message and v1 P2P is followed + - if P2PConnection both supports_v2_p2p and advertise_v2_p2p, TestNode sends ellswift bytes and v2 P2P is followed + - if P2PConnection doesn't supports_v2_p2p but advertise_v2_p2p, + TestNode sends ellswift bytes and P2PConnection disconnects, + TestNode reconnects by sending version message and v1 P2P is followed """ def addconnection_callback(address, port): self.log.debug("Connecting to %s:%d %s" % (address, port, connection_type)) - self.addconnection('%s:%d' % (address, port), connection_type) + self.addconnection('%s:%d' % (address, port), connection_type, advertise_v2_p2p) + + if supports_v2_p2p is None: + supports_v2_p2p = self.use_v2transport + if advertise_v2_p2p is None: + advertise_v2_p2p = self.use_v2transport - p2p_conn.peer_accept_connection(connect_cb=addconnection_callback, connect_id=p2p_idx + 1, net=self.chain, timeout_factor=self.timeout_factor, **kwargs)() + if advertise_v2_p2p: + kwargs['services'] = kwargs.get('services', P2P_SERVICES) | NODE_P2P_V2 + assert self.use_v2transport # only a v2 TestNode could make a v2 outbound connection - if connection_type == "feeler": + # if P2PConnection is advertised to support v2 P2P when it doesn't actually support v2 P2P, + # reconnection needs to be attempted using v1 P2P by sending version message + reconnect = advertise_v2_p2p and not supports_v2_p2p + # P2PConnection needs to be advertised to support v2 P2P so that ellswift bytes are sent instead of msg_version + supports_v2_p2p = supports_v2_p2p and advertise_v2_p2p + p2p_conn.peer_accept_connection(connect_cb=addconnection_callback, connect_id=p2p_idx + 1, net=self.chain, timeout_factor=self.timeout_factor, supports_v2_p2p=supports_v2_p2p, reconnect=reconnect, **kwargs)() + + if reconnect: + p2p_conn.wait_for_reconnect() + + if connection_type == "feeler" or wait_for_disconnect: # feeler connections are closed as soon as the node receives a `version` message p2p_conn.wait_until(lambda: p2p_conn.message_count["version"] == 1, check_connected=False) p2p_conn.wait_until(lambda: not p2p_conn.is_connected, check_connected=False) @@ -700,6 +860,9 @@ def addconnection_callback(address, port): p2p_conn.wait_for_connect() self.p2ps.append(p2p_conn) + if supports_v2_p2p: + p2p_conn.wait_until(lambda: p2p_conn.v2_state.tried_v2_handshake) + p2p_conn.wait_until(lambda: not p2p_conn.on_connection_send_msg) if wait_for_verack: p2p_conn.wait_for_verack() p2p_conn.sync_with_ping() @@ -712,12 +875,13 @@ def num_test_p2p_connections(self): def disconnect_p2ps(self): """Close all p2p connections to the node. - Use only after each p2p has sent a version message to ensure the wait works.""" + The state of the peers (such as txrequests) may not be fully cleared + yet, even after this method returns.""" for p in self.p2ps: p.peer_disconnect() del self.p2ps[:] - wait_until_helper_internal(lambda: self.num_test_p2p_connections() == 0, timeout_factor=self.timeout_factor) + self.wait_until(lambda: self.num_test_p2p_connections() == 0) def bumpmocktime(self, seconds): """Fast forward using setmocktime to self.mocktime + seconds. Requires setmocktime to have @@ -726,6 +890,9 @@ def bumpmocktime(self, seconds): self.mocktime += seconds self.setmocktime(self.mocktime) + def wait_until(self, test_function, timeout=60, check_interval=0.05): + return wait_until_helper_internal(test_function, timeout=timeout, timeout_factor=self.timeout_factor, check_interval=check_interval) + class TestNodeCLIAttr: def __init__(self, cli, command): @@ -744,7 +911,7 @@ def arg_to_cli(arg): return str(arg).lower() elif arg is None: return 'null' - elif isinstance(arg, dict) or isinstance(arg, list): + elif isinstance(arg, dict) or isinstance(arg, list) or isinstance(arg, tuple): return json.dumps(arg, default=serialization_fallback) else: return str(arg) @@ -752,16 +919,16 @@ def arg_to_cli(arg): class TestNodeCLI(): """Interface to bitcoin-cli for an individual node""" - def __init__(self, binary, datadir): + def __init__(self, binaries, datadir): self.options = [] - self.binary = binary + self.binaries = binaries self.datadir = datadir self.input = None self.log = logging.getLogger('TestFramework.bitcoincli') def __call__(self, *options, input=None): # TestNodeCLI is callable with bitcoin-cli command-line options - cli = TestNodeCLI(self.binary, self.datadir) + cli = TestNodeCLI(self.binaries, self.datadir) cli.options = [str(o) for o in options] cli.input = input return cli @@ -778,19 +945,35 @@ def batch(self, requests): results.append(dict(error=e)) return results - def send_cli(self, command=None, *args, **kwargs): + def send_cli(self, clicommand=None, *args, **kwargs): """Run bitcoin-cli command. Deserializes returned string as python object.""" pos_args = [arg_to_cli(arg) for arg in args] - named_args = [str(key) + "=" + arg_to_cli(value) for (key, value) in kwargs.items()] - p_args = [self.binary, f"-datadir={self.datadir}"] + self.options + named_args = [key + "=" + arg_to_cli(value) for (key, value) in kwargs.items() if value is not None] + p_args = self.binaries.rpc_argv() + [f"-datadir={self.datadir}"] + self.options if named_args: p_args += ["-named"] - if command is not None: - p_args += [command] + base_arg_pos = len(p_args) + if clicommand is not None: + p_args += [clicommand] p_args += pos_args + named_args + + # TEST_CLI_MAX_ARG_SIZE is set low enough that checking the string + # length is enough and encoding to bytes is not needed before + # calculating the sum. + sum_arg_size = sum(len(arg) for arg in p_args) + stdin_data = self.input + if sum_arg_size >= TEST_CLI_MAX_ARG_SIZE: + self.log.debug(f"Cli: Command size {sum_arg_size} too large, using stdin") + rpc_args = "\n".join([arg for arg in p_args[base_arg_pos:]]) + if stdin_data is not None: + stdin_data += "\n" + rpc_args + else: + stdin_data = rpc_args + p_args = p_args[:base_arg_pos] + ['-stdin'] + self.log.debug("Running bitcoin-cli {}".format(p_args[2:])) process = subprocess.Popen(p_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) - cli_stdout, cli_stderr = process.communicate(input=self.input) + cli_stdout, cli_stderr = process.communicate(input=stdin_data) returncode = process.poll() if returncode: match = re.match(r'error code: ([-0-9]+)\nerror message:\n(.*)', cli_stderr) @@ -798,95 +981,10 @@ def send_cli(self, command=None, *args, **kwargs): code, message = match.groups() raise JSONRPCException(dict(code=int(code), message=message)) # Ignore cli_stdout, raise with cli_stderr - raise subprocess.CalledProcessError(returncode, self.binary, output=cli_stderr) + raise subprocess.CalledProcessError(returncode, p_args, output=cli_stderr) try: + if not cli_stdout.strip(): + return None return json.loads(cli_stdout, parse_float=decimal.Decimal) except (json.JSONDecodeError, decimal.InvalidOperation): return cli_stdout.rstrip("\n") - -class RPCOverloadWrapper(): - def __init__(self, rpc, cli=False, descriptors=False): - self.rpc = rpc - self.is_cli = cli - self.descriptors = descriptors - - def __getattr__(self, name): - return getattr(self.rpc, name) - - def createwallet_passthrough(self, *args, **kwargs): - return self.__getattr__("createwallet")(*args, **kwargs) - - def createwallet(self, wallet_name, disable_private_keys=None, blank=None, passphrase='', avoid_reuse=None, descriptors=None, load_on_startup=None, external_signer=None): - if descriptors is None: - descriptors = self.descriptors - return self.__getattr__('createwallet')(wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup, external_signer) - - def importprivkey(self, privkey, label=None, rescan=None): - wallet_info = self.getwalletinfo() - if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']): - return self.__getattr__('importprivkey')(privkey, label, rescan) - desc = descsum_create('combo(' + privkey + ')') - req = [{ - 'desc': desc, - 'timestamp': 0 if rescan else 'now', - 'label': label if label else '' - }] - import_res = self.importdescriptors(req) - if not import_res[0]['success']: - raise JSONRPCException(import_res[0]['error']) - - def addmultisigaddress(self, nrequired, keys, label=None, address_type=None): - wallet_info = self.getwalletinfo() - if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']): - return self.__getattr__('addmultisigaddress')(nrequired, keys, label, address_type) - cms = self.createmultisig(nrequired, keys, address_type) - req = [{ - 'desc': cms['descriptor'], - 'timestamp': 0, - 'label': label if label else '' - }] - import_res = self.importdescriptors(req) - if not import_res[0]['success']: - raise JSONRPCException(import_res[0]['error']) - return cms - - def importpubkey(self, pubkey, label=None, rescan=None): - wallet_info = self.getwalletinfo() - if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']): - return self.__getattr__('importpubkey')(pubkey, label, rescan) - desc = descsum_create('combo(' + pubkey + ')') - req = [{ - 'desc': desc, - 'timestamp': 0 if rescan else 'now', - 'label': label if label else '' - }] - import_res = self.importdescriptors(req) - if not import_res[0]['success']: - raise JSONRPCException(import_res[0]['error']) - - def importaddress(self, address, label=None, rescan=None, p2sh=None): - wallet_info = self.getwalletinfo() - if 'descriptors' not in wallet_info or ('descriptors' in wallet_info and not wallet_info['descriptors']): - return self.__getattr__('importaddress')(address, label, rescan, p2sh) - is_hex = False - try: - int(address ,16) - is_hex = True - desc = descsum_create('raw(' + address + ')') - except Exception: - desc = descsum_create('addr(' + address + ')') - reqs = [{ - 'desc': desc, - 'timestamp': 0 if rescan else 'now', - 'label': label if label else '' - }] - if is_hex and p2sh: - reqs.append({ - 'desc': descsum_create('p2sh(raw(' + address + '))'), - 'timestamp': 0 if rescan else 'now', - 'label': label if label else '' - }) - import_res = self.importdescriptors(reqs) - for res in import_res: - if not res['success']: - raise JSONRPCException(res['error']) diff --git a/src/test_framework/test_shell.py b/resources/scenarios/test_framework/test_shell.py similarity index 73% rename from src/test_framework/test_shell.py rename to resources/scenarios/test_framework/test_shell.py index 09ccec28a..9820a222b 100644 --- a/src/test_framework/test_shell.py +++ b/resources/scenarios/test_framework/test_shell.py @@ -2,9 +2,11 @@ # Copyright (c) 2019-2022 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. +import pathlib from test_framework.test_framework import BitcoinTestFramework + class TestShell: """Wrapper Class for BitcoinTestFramework. @@ -16,11 +18,8 @@ class TestShell: start a single TestShell at a time.""" class __TestShell(BitcoinTestFramework): - def add_options(self, parser): - self.add_wallet_options(parser) - def set_test_params(self): - pass + self.uses_wallet = None def run_test(self): pass @@ -59,7 +58,8 @@ def reset(self): print("Shutdown TestShell before resetting!") else: self.num_nodes = None - super().__init__() + dummy_testshell_file = pathlib.Path(__file__).absolute().parent.parent / "testshell_dummy.py" + super().__init__(dummy_testshell_file) instance = None @@ -67,7 +67,13 @@ def __new__(cls): # This implementation enforces singleton pattern, and will return the # previously initialized instance if available if not TestShell.instance: - TestShell.instance = TestShell.__TestShell() + # BitcoinTestFramework instances are supposed to be constructed with the path + # of the calling test in order to find shared data like configuration and the + # cache. Since TestShell is meant for interactive use, there is no concrete + # test; passing a dummy name is fine though, as only the containing directory + # is relevant for successful initialization. + dummy_testshell_file = pathlib.Path(__file__).absolute().parent.parent / "testshell_dummy.py" + TestShell.instance = TestShell.__TestShell(dummy_testshell_file) TestShell.instance.running = False return TestShell.instance diff --git a/src/test_framework/util.py b/resources/scenarios/test_framework/util.py similarity index 78% rename from src/test_framework/util.py rename to resources/scenarios/test_framework/util.py index 61346e9d1..e5a5938f0 100644 --- a/src/test_framework/util.py +++ b/resources/scenarios/test_framework/util.py @@ -5,7 +5,7 @@ """Helpful routines for regression testing.""" from base64 import b64encode -from decimal import Decimal, ROUND_DOWN +from decimal import Decimal from subprocess import CalledProcessError import hashlib import inspect @@ -13,14 +13,18 @@ import logging import os import pathlib +import platform import random import re -import sys import time from . import coverage from .authproxy import AuthServiceProxy, JSONRPCException -from typing import Callable, Optional, Tuple +from .descriptors import descsum_create +from collections.abc import Callable +from typing import Optional, Union + +SATOSHI_PRECISION = Decimal('0.00000001') logger = logging.getLogger("TestFramework.utils") @@ -52,10 +56,31 @@ def assert_fee_amount(fee, tx_size, feerate_BTC_kvB): raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)" % (str(fee), str(target_fee))) +def summarise_dict_differences(thing1, thing2): + if not isinstance(thing1, dict) or not isinstance(thing2, dict): + return thing1, thing2 + d1, d2 = {}, {} + for k in sorted(thing1.keys()): + if k not in thing2: + d1[k] = thing1[k] + elif thing1[k] != thing2[k]: + d1[k], d2[k] = summarise_dict_differences(thing1[k], thing2[k]) + for k in sorted(thing2.keys()): + if k not in thing1: + d2[k] = thing2[k] + return d1, d2 + def assert_equal(thing1, thing2, *args): + if thing1 != thing2 and not args and isinstance(thing1, dict) and isinstance(thing2, dict): + d1,d2 = summarise_dict_differences(thing1, thing2) + raise AssertionError("not(%s == %s)\n in particular not(%s == %s)" % (thing1, thing2, d1, d2)) if thing1 != thing2 or any(thing1 != arg for arg in args): raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args)) +def assert_not_equal(thing1, thing2, *, error_message=""): + if thing1 == thing2: + raise AssertionError(f"Both values are {thing1}{f', {error_message}' if error_message else ''}") + def assert_greater_than(thing1, thing2): if thing1 <= thing2: @@ -77,10 +102,9 @@ def assert_raises_message(exc, message, fun, *args, **kwds): except JSONRPCException: raise AssertionError("Use assert_raises_rpc_error() to test RPC failures") except exc as e: - if message is not None and message not in e.error['message']: - raise AssertionError( - "Expected substring not found in error message:\nsubstring: '{}'\nerror message: '{}'.".format( - message, e.error['message'])) + if message is not None and message not in str(e): + raise AssertionError("Expected substring not found in exception:\n" + f"substring: '{message}'\nexception: {e!r}.") except Exception as e: raise AssertionError("Unexpected exception raised: " + type(e).__name__) else: @@ -230,6 +254,12 @@ def ceildiv(a, b): return -(-a // b) +def random_bitflip(data): + data = list(data) + data[random.randrange(len(data))] ^= (1 << (random.randrange(8))) + return bytes(data) + + def get_fee(tx_size, feerate_btc_kvb): """Calculate the fee in BTC given a feerate is BTC/kvB. Reflects CFeeRate::GetFee""" feerate_sat_kvb = int(feerate_btc_kvb * Decimal(1e8)) # Fee in sat/kvb as an int to avoid float precision errors @@ -237,11 +267,33 @@ def get_fee(tx_size, feerate_btc_kvb): return target_fee_sat / Decimal(1e8) # Return result in BTC -def satoshi_round(amount): - return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) +def satoshi_round(amount: Union[int, float, str], *, rounding: str) -> Decimal: + """Rounds a Decimal amount to the nearest satoshi using the specified rounding mode.""" + return Decimal(amount).quantize(SATOSHI_PRECISION, rounding=rounding) + +def ensure_for(*, duration, f, check_interval=0.2): + """Check if the predicate keeps returning True for duration. -def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=float('inf'), lock=None, timeout_factor=1.0): + check_interval can be used to configure the wait time between checks. + Setting check_interval to 0 will allow to have two checks: one in the + beginning and one after duration. + """ + # If check_interval is 0 or negative or larger than duration, we fall back + # to checking once in the beginning and once at the end of duration + if check_interval <= 0 or check_interval > duration: + check_interval = duration + time_end = time.time() + duration + predicate_source = "''''\n" + inspect.getsource(f) + "'''" + while True: + if not f(): + raise AssertionError(f"Predicate {predicate_source} became false within {duration} seconds") + if time.time() > time_end: + return + time.sleep(check_interval) + + +def wait_until_helper_internal(predicate, *, timeout=60, lock=None, timeout_factor=1.0, check_interval=0.05): """Sleep until the predicate resolves to be True. Warning: Note that this method is not recommended to be used in tests as it is @@ -250,13 +302,10 @@ def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=floa properly scaled. Furthermore, `wait_until()` from `P2PInterface` class in `p2p.py` has a preset lock. """ - if attempts == float('inf') and timeout == float('inf'): - timeout = 60 timeout = timeout * timeout_factor - attempt = 0 time_end = time.time() + timeout - while attempt < attempts and time.time() < time_end: + while time.time() < time_end: if lock: with lock: if predicate(): @@ -264,17 +313,19 @@ def wait_until_helper_internal(predicate, *, attempts=float('inf'), timeout=floa else: if predicate(): return - attempt += 1 - time.sleep(0.05) + time.sleep(check_interval) # Print the cause of the timeout predicate_source = "''''\n" + inspect.getsource(predicate) + "'''" logger.error("wait_until() failed. Predicate: {}".format(predicate_source)) - if attempt >= attempts: - raise AssertionError("Predicate {} not true after {} attempts".format(predicate_source, attempts)) - elif time.time() >= time_end: - raise AssertionError("Predicate {} not true after {} seconds".format(predicate_source, timeout)) - raise RuntimeError('Unreachable') + raise AssertionError("Predicate {} not true after {} seconds".format(predicate_source, timeout)) + + +def bpf_cflags(): + return [ + "-Wno-error=implicit-function-declaration", + "-Wno-duplicate-decl-specifier", # https://github.com/bitcoin/bitcoin/issues/32322 + ] def sha256sum_file(filename): @@ -287,10 +338,11 @@ def sha256sum_file(filename): return h.digest() -# TODO: Remove and use random.randbytes(n) instead, available in Python 3.9 -def random_bytes(n): - """Return a random bytes object of length n.""" - return bytes(random.getrandbits(8) for i in range(n)) +def util_xor(data, key, *, offset): + data = bytearray(data) + for i in range(len(data)): + data[i] ^= key[(i + offset) % len(key)] + return bytes(data) # RPC/P2P connection constants and functions @@ -298,9 +350,9 @@ def random_bytes(n): # The maximum number of nodes a single test can spawn MAX_NODES = 12 -# Don't assign rpc or p2p ports lower than this +# Don't assign p2p, rpc or tor ports lower than this PORT_MIN = int(os.getenv('TEST_RUNNER_PORT_MIN', default=11000)) -# The number of ports to "reserve" for p2p and rpc, each +# The number of ports to "reserve" for p2p, rpc and tor, each PORT_RANGE = 5000 @@ -340,7 +392,11 @@ def p2p_port(n): def rpc_port(n): - return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES) + return p2p_port(n) + PORT_RANGE + + +def tor_port(n): + return p2p_port(n) + PORT_RANGE * 2 def rpc_url(datadir, i, chain, rpchost): @@ -401,14 +457,23 @@ def write_config(config_path, *, n, chain, extra_config="", disable_autoconnect= # in tests. f.write("peertimeout=999999999\n") f.write("printtoconsole=0\n") - f.write("upnp=0\n") f.write("natpmp=0\n") f.write("shrinkdebugfile=0\n") - f.write("deprecatedrpc=create_bdb\n") # Required to run the tests # To improve SQLite wallet performance so that the tests don't timeout, use -unsafesqlitesync f.write("unsafesqlitesync=1\n") if disable_autoconnect: f.write("connect=0\n") + # Limit max connections to mitigate test failures on some systems caused by the warning: + # "Warning: Reducing -maxconnections from <...> to <...> due to system limitations". + # The value is calculated as follows: + # available_fds = 256 // Same as FD_SETSIZE on NetBSD. + # MIN_CORE_FDS = 151 // Number of file descriptors required for core functionality. + # MAX_ADDNODE_CONNECTIONS = 8 // Maximum number of -addnode outgoing nodes. + # nBind == 3 // Maximum number of bound interfaces used in a test. + # + # min_required_fds = MIN_CORE_FDS + MAX_ADDNODE_CONNECTIONS + nBind = 151 + 8 + 3 = 162; + # nMaxConnections = available_fds - min_required_fds = 256 - 161 = 94; + f.write("maxconnections=94\n") f.write(extra_config) @@ -416,16 +481,16 @@ def get_datadir_path(dirname, n): return pathlib.Path(dirname) / f"node{n}" -def get_temp_default_datadir(temp_dir: pathlib.Path) -> Tuple[dict, pathlib.Path]: +def get_temp_default_datadir(temp_dir: pathlib.Path) -> tuple[dict, pathlib.Path]: """Return os-specific environment variables that can be set to make the GetDefaultDataDir() function return a datadir path under the provided temp_dir, as well as the complete path it would return.""" - if sys.platform == "win32": + if platform.system() == "Windows": env = dict(APPDATA=str(temp_dir)) datadir = temp_dir / "Bitcoin" else: env = dict(HOME=str(temp_dir)) - if sys.platform == "darwin": + if platform.system() == "Darwin": datadir = temp_dir / "Library/Application Support/Bitcoin" else: datadir = temp_dir / ".bitcoin" @@ -490,18 +555,6 @@ def check_node_connections(*, node, num_in, num_out): ############################# -def find_output(node, txid, amount, *, blockhash=None): - """ - Return index to output of txid with value amount - Raises exception if there is none. - """ - txdata = node.getrawtransaction(txid, 1, blockhash) - for i in range(len(txdata["vout"])): - if txdata["vout"][i]["value"] == amount: - return i - raise RuntimeError("find_output txid %s : %s not found" % (txid, str(amount))) - - # Create large OP_RETURN txouts that can be appended to a transaction # to make it large (helper for constructing large transactions). The # total serialized size of the txouts is about 66k vbytes. @@ -549,3 +602,20 @@ def find_vout_for_address(node, txid, addr): if addr == tx["vout"][i]["scriptPubKey"]["address"]: return i raise RuntimeError("Vout not found for address: txid=%s, addr=%s" % (txid, addr)) + + +def sync_txindex(test_framework, node): + test_framework.log.debug("Waiting for node txindex to sync") + sync_start = int(time.time()) + test_framework.wait_until(lambda: node.getindexinfo("txindex")["txindex"]["synced"]) + test_framework.log.debug(f"Synced in {time.time() - sync_start} seconds") + +def wallet_importprivkey(wallet_rpc, privkey, timestamp, *, label=""): + desc = descsum_create("combo(" + privkey + ")") + req = [{ + "desc": desc, + "timestamp": timestamp, + "label": label, + }] + import_res = wallet_rpc.importdescriptors(req) + assert_equal(import_res[0]["success"], True) diff --git a/resources/scenarios/test_framework/v2_p2p.py b/resources/scenarios/test_framework/v2_p2p.py new file mode 100644 index 000000000..87600c36d --- /dev/null +++ b/resources/scenarios/test_framework/v2_p2p.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +# Copyright (c) 2022 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Class for v2 P2P protocol (see BIP 324)""" + +import random + +from .crypto.bip324_cipher import FSChaCha20Poly1305 +from .crypto.chacha20 import FSChaCha20 +from .crypto.ellswift import ellswift_create, ellswift_ecdh_xonly +from .crypto.hkdf import hkdf_sha256 +from .key import TaggedHash +from .messages import MAGIC_BYTES + + +CHACHA20POLY1305_EXPANSION = 16 +HEADER_LEN = 1 +IGNORE_BIT_POS = 7 +LENGTH_FIELD_LEN = 3 +MAX_GARBAGE_LEN = 4095 + +SHORTID = { + 1: b"addr", + 2: b"block", + 3: b"blocktxn", + 4: b"cmpctblock", + 5: b"feefilter", + 6: b"filteradd", + 7: b"filterclear", + 8: b"filterload", + 9: b"getblocks", + 10: b"getblocktxn", + 11: b"getdata", + 12: b"getheaders", + 13: b"headers", + 14: b"inv", + 15: b"mempool", + 16: b"merkleblock", + 17: b"notfound", + 18: b"ping", + 19: b"pong", + 20: b"sendcmpct", + 21: b"tx", + 22: b"getcfilters", + 23: b"cfilter", + 24: b"getcfheaders", + 25: b"cfheaders", + 26: b"getcfcheckpt", + 27: b"cfcheckpt", + 28: b"addrv2", +} + +# Dictionary which contains short message type ID for the P2P message +MSGTYPE_TO_SHORTID = {msgtype: shortid for shortid, msgtype in SHORTID.items()} + + +class EncryptedP2PState: + """A class for managing the state when v2 P2P protocol is used. Performs initial v2 handshake and encrypts/decrypts + P2P messages. P2PConnection uses an object of this class. + + + Args: + initiating (bool): defines whether the P2PConnection is an initiator or responder. + - initiating = True for inbound connections in the test framework [TestNode <------- P2PConnection] + - initiating = False for outbound connections in the test framework [TestNode -------> P2PConnection] + + net (string): chain used (regtest, signet etc..) + + Methods: + perform an advanced form of diffie-hellman handshake to instantiate the encrypted transport. before exchanging + any P2P messages, 2 nodes perform this handshake in order to determine a shared secret that is unique to both + of them and use it to derive keys to encrypt/decrypt P2P messages. + - initial v2 handshakes is performed by: (see BIP324 section #overall-handshake-pseudocode) + 1. initiator using initiate_v2_handshake(), complete_handshake() and authenticate_handshake() + 2. responder using respond_v2_handshake(), complete_handshake() and authenticate_handshake() + - initialize_v2_transport() sets various BIP324 derived keys and ciphers. + + encrypt/decrypt v2 P2P messages using v2_enc_packet() and v2_receive_packet(). + """ + def __init__(self, *, initiating, net): + self.initiating = initiating # True if initiator + self.net = net + self.peer = {} # object with various BIP324 derived keys and ciphers + self.privkey_ours = None + self.ellswift_ours = None + self.sent_garbage = b"" + self.received_garbage = b"" + self.received_prefix = b"" # received ellswift bytes till the first mismatch from 16 bytes v1_prefix + self.tried_v2_handshake = False # True when the initial handshake is over + # stores length of packet contents to detect whether first 3 bytes (which contains length of packet contents) + # has been decrypted. set to -1 if decryption hasn't been done yet. + self.contents_len = -1 + self.found_garbage_terminator = False + self.transport_version = b'' + + @staticmethod + def v2_ecdh(priv, ellswift_theirs, ellswift_ours, initiating): + """Compute BIP324 shared secret. + + Returns: + bytes - BIP324 shared secret + """ + ecdh_point_x32 = ellswift_ecdh_xonly(ellswift_theirs, priv) + if initiating: + # Initiating, place our public key encoding first. + return TaggedHash("bip324_ellswift_xonly_ecdh", ellswift_ours + ellswift_theirs + ecdh_point_x32) + else: + # Responding, place their public key encoding first. + return TaggedHash("bip324_ellswift_xonly_ecdh", ellswift_theirs + ellswift_ours + ecdh_point_x32) + + def generate_keypair_and_garbage(self, garbage_len=None): + """Generates ellswift keypair and 4095 bytes garbage at max""" + self.privkey_ours, self.ellswift_ours = ellswift_create() + if garbage_len is None: + garbage_len = random.randrange(MAX_GARBAGE_LEN + 1) + self.sent_garbage = random.randbytes(garbage_len) + return self.ellswift_ours + self.sent_garbage + + def initiate_v2_handshake(self): + """Initiator begins the v2 handshake by sending its ellswift bytes and garbage + + Returns: + bytes - bytes to be sent to the peer when starting the v2 handshake as an initiator + """ + return self.generate_keypair_and_garbage() + + def respond_v2_handshake(self, response): + """Responder begins the v2 handshake by sending its ellswift bytes and garbage. However, the responder + sends this after having received at least one byte that mismatches 16-byte v1_prefix. + + Returns: + 1. int - length of bytes that were consumed so that recvbuf can be updated + 2. bytes - bytes to be sent to the peer when starting the v2 handshake as a responder. + - returns b"" if more bytes need to be received before we can respond and start the v2 handshake. + - returns -1 to downgrade the connection to v1 P2P. + """ + v1_prefix = MAGIC_BYTES[self.net] + b'version\x00\x00\x00\x00\x00' + while len(self.received_prefix) < 16: + byte = response.read(1) + # return b"" if we need to receive more bytes + if not byte: + return len(self.received_prefix), b"" + self.received_prefix += byte + if self.received_prefix[-1] != v1_prefix[len(self.received_prefix) - 1]: + return len(self.received_prefix), self.generate_keypair_and_garbage() + # return -1 to decide v1 only after all 16 bytes processed + return len(self.received_prefix), -1 + + def complete_handshake(self, response): + """ Instantiates the encrypted transport and + sends garbage terminator + optional decoy packets + transport version packet. + Done by both initiator and responder. + + Returns: + 1. int - length of bytes that were consumed. returns 0 if all 64 bytes from ellswift haven't been received yet. + 2. bytes - bytes to be sent to the peer when completing the v2 handshake + """ + ellswift_theirs = self.received_prefix + response.read(64 - len(self.received_prefix)) + # return b"" if we need to receive more bytes + if len(ellswift_theirs) != 64: + return 0, b"" + ecdh_secret = self.v2_ecdh(self.privkey_ours, ellswift_theirs, self.ellswift_ours, self.initiating) + self.initialize_v2_transport(ecdh_secret) + # Send garbage terminator + msg_to_send = self.peer['send_garbage_terminator'] + # Optionally send decoy packets after garbage terminator. + aad = self.sent_garbage + for decoy_content_len in [random.randint(1, 100) for _ in range(random.randint(0, 10))]: + msg_to_send += self.v2_enc_packet(decoy_content_len * b'\x00', aad=aad, ignore=True) + aad = b'' + # Send version packet. + msg_to_send += self.v2_enc_packet(self.transport_version, aad=aad) + return 64 - len(self.received_prefix), msg_to_send + + def authenticate_handshake(self, response): + """ Ensures that the received optional decoy packets and transport version packet are authenticated. + Marks the v2 handshake as complete. Done by both initiator and responder. + + Returns: + 1. int - length of bytes that were processed so that recvbuf can be updated + 2. bool - True if the authentication was successful/more bytes need to be received and False otherwise + """ + processed_length = 0 + + # Detect garbage terminator in the received bytes + if not self.found_garbage_terminator: + received_garbage = response[:16] + response = response[16:] + processed_length = len(received_garbage) + for i in range(MAX_GARBAGE_LEN + 1): + if received_garbage[-16:] == self.peer['recv_garbage_terminator']: + # Receive, decode, and ignore version packet. + # This includes skipping decoys and authenticating the received garbage. + self.found_garbage_terminator = True + self.received_garbage = received_garbage[:-16] + break + else: + # don't update recvbuf since more bytes need to be received + if len(response) == 0: + return 0, True + received_garbage += response[:1] + processed_length += 1 + response = response[1:] + else: + # disconnect since garbage terminator was not seen after 4 KiB of garbage. + return processed_length, False + + # Process optional decoy packets and transport version packet + while not self.tried_v2_handshake: + length, contents = self.v2_receive_packet(response, aad=self.received_garbage) + if length == -1: + return processed_length, False + elif length == 0: + return processed_length, True + processed_length += length + self.received_garbage = b"" + # decoy packets have contents = None. v2 handshake is complete only when version packet + # (can be empty with contents = b"") with contents != None is received. + if contents is not None: + assert contents == b"" # currently TestNode sends an empty version packet + self.tried_v2_handshake = True + return processed_length, True + response = response[length:] + + def initialize_v2_transport(self, ecdh_secret): + """Sets the peer object with various BIP324 derived keys and ciphers.""" + peer = {} + salt = b'bitcoin_v2_shared_secret' + MAGIC_BYTES[self.net] + for name in ('initiator_L', 'initiator_P', 'responder_L', 'responder_P', 'garbage_terminators', 'session_id'): + peer[name] = hkdf_sha256(salt=salt, ikm=ecdh_secret, info=name.encode('utf-8'), length=32) + if self.initiating: + self.peer['send_L'] = FSChaCha20(peer['initiator_L']) + self.peer['send_P'] = FSChaCha20Poly1305(peer['initiator_P']) + self.peer['send_garbage_terminator'] = peer['garbage_terminators'][:16] + self.peer['recv_L'] = FSChaCha20(peer['responder_L']) + self.peer['recv_P'] = FSChaCha20Poly1305(peer['responder_P']) + self.peer['recv_garbage_terminator'] = peer['garbage_terminators'][16:] + else: + self.peer['send_L'] = FSChaCha20(peer['responder_L']) + self.peer['send_P'] = FSChaCha20Poly1305(peer['responder_P']) + self.peer['send_garbage_terminator'] = peer['garbage_terminators'][16:] + self.peer['recv_L'] = FSChaCha20(peer['initiator_L']) + self.peer['recv_P'] = FSChaCha20Poly1305(peer['initiator_P']) + self.peer['recv_garbage_terminator'] = peer['garbage_terminators'][:16] + self.peer['session_id'] = peer['session_id'] + + def v2_enc_packet(self, contents, aad=b'', ignore=False): + """Encrypt a BIP324 packet. + + Returns: + bytes - encrypted packet contents + """ + assert len(contents) <= 2**24 - 1 + header = (ignore << IGNORE_BIT_POS).to_bytes(HEADER_LEN, 'little') + plaintext = header + contents + aead_ciphertext = self.peer['send_P'].encrypt(aad, plaintext) + enc_plaintext_len = self.peer['send_L'].crypt(len(contents).to_bytes(LENGTH_FIELD_LEN, 'little')) + return enc_plaintext_len + aead_ciphertext + + def v2_receive_packet(self, response, aad=b''): + """Decrypt a BIP324 packet + + Returns: + 1. int - number of bytes consumed (or -1 if error) + 2. bytes - contents of decrypted non-decoy packet if any (or None otherwise) + """ + if self.contents_len == -1: + if len(response) < LENGTH_FIELD_LEN: + return 0, None + enc_contents_len = response[:LENGTH_FIELD_LEN] + self.contents_len = int.from_bytes(self.peer['recv_L'].crypt(enc_contents_len), 'little') + response = response[LENGTH_FIELD_LEN:] + if len(response) < HEADER_LEN + self.contents_len + CHACHA20POLY1305_EXPANSION: + return 0, None + aead_ciphertext = response[:HEADER_LEN + self.contents_len + CHACHA20POLY1305_EXPANSION] + plaintext = self.peer['recv_P'].decrypt(aad, aead_ciphertext) + if plaintext is None: + return -1, None # disconnect + header = plaintext[:HEADER_LEN] + length = LENGTH_FIELD_LEN + HEADER_LEN + self.contents_len + CHACHA20POLY1305_EXPANSION + self.contents_len = -1 + return length, None if (header[0] & (1 << IGNORE_BIT_POS)) else plaintext[HEADER_LEN:] diff --git a/src/test_framework/wallet.py b/resources/scenarios/test_framework/wallet.py similarity index 88% rename from src/test_framework/wallet.py rename to resources/scenarios/test_framework/wallet.py index 035a482f4..a47ccab01 100644 --- a/src/test_framework/wallet.py +++ b/resources/scenarios/test_framework/wallet.py @@ -9,7 +9,6 @@ from enum import Enum from typing import ( Any, - List, Optional, ) from test_framework.address import ( @@ -33,10 +32,10 @@ CTxIn, CTxInWitness, CTxOut, + hash256, ) from test_framework.script import ( CScript, - LEAF_VERSION_TAPSCRIPT, OP_NOP, OP_RETURN, OP_TRUE, @@ -44,6 +43,7 @@ taproot_construct, ) from test_framework.script_util import ( + bulk_vout, key_to_p2pk_script, key_to_p2pkh_script, key_to_p2sh_p2wpkh_script, @@ -52,6 +52,7 @@ from test_framework.util import ( assert_equal, assert_greater_than_or_equal, + get_fee, ) from test_framework.wallet_util import generate_keypair @@ -66,7 +67,10 @@ class MiniWalletMode(Enum): However, if the transactions need to be modified by the user (e.g. prepending scriptSig for testing opcodes that are activated by a soft-fork), or the txs should contain an actual signature, the raw modes RAW_OP_TRUE and RAW_P2PK - can be useful. Summary of modes: + can be useful. In order to avoid mixing of UTXOs between different MiniWallet + instances, a tag name can be passed to the default mode, to create different + output scripts. Note that the UTXOs from the pre-generated test chain can + only be spent if no tag is passed. Summary of modes: | output | | tx is | can modify | needs mode | description | address | standard | scriptSig | signing @@ -81,22 +85,25 @@ class MiniWalletMode(Enum): class MiniWallet: - def __init__(self, test_node, *, mode=MiniWalletMode.ADDRESS_OP_TRUE): + def __init__(self, test_node, *, mode=MiniWalletMode.ADDRESS_OP_TRUE, tag_name=None): self._test_node = test_node self._utxos = [] self._mode = mode assert isinstance(mode, MiniWalletMode) if mode == MiniWalletMode.RAW_OP_TRUE: + assert tag_name is None self._scriptPubKey = bytes(CScript([OP_TRUE])) elif mode == MiniWalletMode.RAW_P2PK: # use simple deterministic private key (k=1) + assert tag_name is None self._priv_key = ECKey() self._priv_key.set((1).to_bytes(32, 'big'), True) pub_key = self._priv_key.get_pubkey() self._scriptPubKey = key_to_p2pk_script(pub_key.get_bytes()) elif mode == MiniWalletMode.ADDRESS_OP_TRUE: - self._address, self._internal_key = create_deterministic_address_bcrt1_p2tr_op_true() + internal_key = None if tag_name is None else compute_xonly_pubkey(hash256(tag_name.encode()))[0] + self._address, self._taproot_info = create_deterministic_address_bcrt1_p2tr_op_true(internal_key) self._scriptPubKey = address_to_scriptpubkey(self._address) # When the pre-mined test framework chain is used, it contains coinbase @@ -109,17 +116,13 @@ def __init__(self, test_node, *, mode=MiniWalletMode.ADDRESS_OP_TRUE): def _create_utxo(self, *, txid, vout, value, height, coinbase, confirmations): return {"txid": txid, "vout": vout, "value": value, "height": height, "coinbase": coinbase, "confirmations": confirmations} - def _bulk_tx(self, tx, target_weight): - """Pad a transaction with extra outputs until it reaches a target weight (or higher). + def _bulk_tx(self, tx, target_vsize): + """Pad a transaction with extra outputs until it reaches a target vsize. returns the tx """ - tx.vout.append(CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN, b'a']))) - dummy_vbytes = (target_weight - tx.get_weight() + 3) // 4 - tx.vout[-1].scriptPubKey = CScript([OP_RETURN, b'a' * dummy_vbytes]) - # Lower bound should always be off by at most 3 - assert_greater_than_or_equal(tx.get_weight(), target_weight) - # Higher bound should always be off by at most 3 + 12 weight (for encoding the length) - assert_greater_than_or_equal(target_weight + 15, tx.get_weight()) + tx.vout.append(CTxOut(nValue=0, scriptPubKey=CScript([OP_RETURN]))) + bulk_vout(tx, target_vsize) + def get_balance(self): return sum(u['value'] for u in self._utxos) @@ -181,7 +184,12 @@ def sign_tx(self, tx, fixed_length=True): elif self._mode == MiniWalletMode.ADDRESS_OP_TRUE: tx.wit.vtxinwit = [CTxInWitness()] * len(tx.vin) for i in tx.wit.vtxinwit: - i.scriptWitness.stack = [CScript([OP_TRUE]), bytes([LEAF_VERSION_TAPSCRIPT]) + self._internal_key] + assert_equal(len(self._taproot_info.leaves), 1) + leaf_info = list(self._taproot_info.leaves.values())[0] + i.scriptWitness.stack = [ + leaf_info.script, + bytes([leaf_info.version | self._taproot_info.negflag]) + self._taproot_info.internal_pubkey, + ] else: assert False @@ -198,7 +206,7 @@ def generate(self, num_blocks, **kwargs): self.rescan_utxos() return blocks - def get_scriptPubKey(self): + def get_output_script(self): return self._scriptPubKey def get_descriptor(self): @@ -270,7 +278,7 @@ def send_to(self, *, from_node, scriptPubKey, amount, fee=1000): return { "sent_vout": 1, "txid": txid, - "wtxid": tx.getwtxid(), + "wtxid": tx.wtxid_hex, "hex": tx.serialize().hex(), "tx": tx, } @@ -284,14 +292,15 @@ def send_self_transfer_multi(self, *, from_node, **kwargs): def create_self_transfer_multi( self, *, - utxos_to_spend: Optional[List[dict]] = None, + utxos_to_spend: Optional[list[dict]] = None, num_outputs=1, amount_per_output=0, + version=2, locktime=0, sequence=0, fee_per_output=1000, - target_weight=0, - confirmed_only=False + target_vsize=0, + confirmed_only=False, ): """ Create and return a transaction that spends the given UTXOs and creates a @@ -314,14 +323,15 @@ def create_self_transfer_multi( tx = CTransaction() tx.vin = [CTxIn(COutPoint(int(utxo_to_spend['txid'], 16), utxo_to_spend['vout']), nSequence=seq) for utxo_to_spend, seq in zip(utxos_to_spend, sequence)] tx.vout = [CTxOut(amount_per_output, bytearray(self._scriptPubKey)) for _ in range(num_outputs)] + tx.version = version tx.nLockTime = locktime self.sign_tx(tx) - if target_weight: - self._bulk_tx(tx, target_weight) + if target_vsize: + self._bulk_tx(tx, target_vsize) - txid = tx.rehash() + txid = tx.txid_hex return { "new_utxos": [self._create_utxo( txid=txid, @@ -333,19 +343,20 @@ def create_self_transfer_multi( ) for i in range(len(tx.vout))], "fee": fee, "txid": txid, - "wtxid": tx.getwtxid(), + "wtxid": tx.wtxid_hex, "hex": tx.serialize().hex(), "tx": tx, } - def create_self_transfer(self, *, + def create_self_transfer( + self, + *, fee_rate=Decimal("0.003"), fee=Decimal("0"), utxo_to_spend=None, - locktime=0, - sequence=0, - target_weight=0, - confirmed_only=False + target_vsize=0, + confirmed_only=False, + **kwargs, ): """Create and return a tx with the specified fee. If fee is 0, use fee_rate, where the resulting fee may be exact or at most one satoshi higher than needed.""" utxo_to_spend = utxo_to_spend or self.get_utxo(confirmed_only=confirmed_only) @@ -358,11 +369,19 @@ def create_self_transfer(self, *, vsize = Decimal(168) # P2PK (73 bytes scriptSig + 35 bytes scriptPubKey + 60 bytes other) else: assert False + if target_vsize and not fee: # respect fee_rate if target vsize is passed + fee = get_fee(target_vsize, fee_rate) send_value = utxo_to_spend["value"] - (fee or (fee_rate * vsize / 1000)) - + if send_value <= 0: + raise RuntimeError(f"UTXO value {utxo_to_spend['value']} is too small to cover fees {(fee or (fee_rate * vsize / 1000))}") # create tx - tx = self.create_self_transfer_multi(utxos_to_spend=[utxo_to_spend], locktime=locktime, sequence=sequence, amount_per_output=int(COIN * send_value), target_weight=target_weight) - if not target_weight: + tx = self.create_self_transfer_multi( + utxos_to_spend=[utxo_to_spend], + amount_per_output=int(COIN * send_value), + target_vsize=target_vsize, + **kwargs, + ) + if not target_vsize: assert_equal(tx["tx"].get_vsize(), vsize) tx["new_utxo"] = tx.pop("new_utxos")[0] diff --git a/src/test_framework/wallet_util.py b/resources/scenarios/test_framework/wallet_util.py similarity index 62% rename from src/test_framework/wallet_util.py rename to resources/scenarios/test_framework/wallet_util.py index 44811918b..2168e607b 100755 --- a/src/test_framework/wallet_util.py +++ b/resources/scenarios/test_framework/wallet_util.py @@ -4,6 +4,7 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Useful util functions for testing the wallet""" from collections import namedtuple +import unittest from test_framework.address import ( byte_to_base58, @@ -15,6 +16,11 @@ script_to_p2wsh, ) from test_framework.key import ECKey +from test_framework.messages import ( + CTxIn, + CTxInWitness, + WITNESS_SCALE_FACTOR, +) from test_framework.script_util import ( key_to_p2pkh_script, key_to_p2wpkh_script, @@ -123,6 +129,19 @@ def generate_keypair(compressed=True, wif=False): privkey = bytes_to_wif(privkey.get_bytes(), compressed) return privkey, pubkey +def calculate_input_weight(scriptsig_hex, witness_stack_hex=None): + """Given a scriptSig and a list of witness stack items for an input in hex format, + calculate the total input weight. If the input has no witness data, + `witness_stack_hex` can be set to None.""" + tx_in = CTxIn(scriptSig=bytes.fromhex(scriptsig_hex)) + witness_size = 0 + if witness_stack_hex is not None: + tx_inwit = CTxInWitness() + for witness_item_hex in witness_stack_hex: + tx_inwit.scriptWitness.stack.append(bytes.fromhex(witness_item_hex)) + witness_size = len(tx_inwit.serialize()) + return len(tx_in.serialize()) * WITNESS_SCALE_FACTOR + witness_size + class WalletUnlock(): """ A context manager for unlocking a wallet with a passphrase and automatically locking it afterward. @@ -141,3 +160,42 @@ def __enter__(self): def __exit__(self, *args): _ = args self.wallet.walletlock() + + +class TestFrameworkWalletUtil(unittest.TestCase): + def test_calculate_input_weight(self): + SKELETON_BYTES = 32 + 4 + 4 # prevout-txid, prevout-index, sequence + SMALL_LEN_BYTES = 1 # bytes needed for encoding scriptSig / witness item lengths < 253 + LARGE_LEN_BYTES = 3 # bytes needed for encoding scriptSig / witness item lengths >= 253 + + # empty scriptSig, no witness + self.assertEqual(calculate_input_weight(""), + (SKELETON_BYTES + SMALL_LEN_BYTES) * WITNESS_SCALE_FACTOR) + self.assertEqual(calculate_input_weight("", None), + (SKELETON_BYTES + SMALL_LEN_BYTES) * WITNESS_SCALE_FACTOR) + # small scriptSig, no witness + scriptSig_small = "00"*252 + self.assertEqual(calculate_input_weight(scriptSig_small, None), + (SKELETON_BYTES + SMALL_LEN_BYTES + 252) * WITNESS_SCALE_FACTOR) + # small scriptSig, empty witness stack + self.assertEqual(calculate_input_weight(scriptSig_small, []), + (SKELETON_BYTES + SMALL_LEN_BYTES + 252) * WITNESS_SCALE_FACTOR + SMALL_LEN_BYTES) + # large scriptSig, no witness + scriptSig_large = "00"*253 + self.assertEqual(calculate_input_weight(scriptSig_large, None), + (SKELETON_BYTES + LARGE_LEN_BYTES + 253) * WITNESS_SCALE_FACTOR) + # large scriptSig, empty witness stack + self.assertEqual(calculate_input_weight(scriptSig_large, []), + (SKELETON_BYTES + LARGE_LEN_BYTES + 253) * WITNESS_SCALE_FACTOR + SMALL_LEN_BYTES) + # empty scriptSig, 5 small witness stack items + self.assertEqual(calculate_input_weight("", ["00", "11", "22", "33", "44"]), + ((SKELETON_BYTES + SMALL_LEN_BYTES) * WITNESS_SCALE_FACTOR) + SMALL_LEN_BYTES + 5 * SMALL_LEN_BYTES + 5) + # empty scriptSig, 253 small witness stack items + self.assertEqual(calculate_input_weight("", ["00"]*253), + ((SKELETON_BYTES + SMALL_LEN_BYTES) * WITNESS_SCALE_FACTOR) + LARGE_LEN_BYTES + 253 * SMALL_LEN_BYTES + 253) + # small scriptSig, 3 large witness stack items + self.assertEqual(calculate_input_weight(scriptSig_small, ["00"*253]*3), + ((SKELETON_BYTES + SMALL_LEN_BYTES + 252) * WITNESS_SCALE_FACTOR) + SMALL_LEN_BYTES + 3 * LARGE_LEN_BYTES + 3*253) + # large scriptSig, 3 large witness stack items + self.assertEqual(calculate_input_weight(scriptSig_large, ["00"*253]*3), + ((SKELETON_BYTES + LARGE_LEN_BYTES + 253) * WITNESS_SCALE_FACTOR) + SMALL_LEN_BYTES + 3 * LARGE_LEN_BYTES + 3*253) diff --git a/resources/scenarios/test_scenarios/__init__.py b/resources/scenarios/test_scenarios/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/resources/scenarios/test_scenarios/buggy_failure.py b/resources/scenarios/test_scenarios/buggy_failure.py new file mode 100644 index 000000000..fbda306d7 --- /dev/null +++ b/resources/scenarios/test_scenarios/buggy_failure.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + + +# The base class exists inside the commander container +try: + from commander import Commander +except Exception: + from resources.scenarios.commander import Commander + + +class Failure(Commander): + def set_test_params(self): + self.num_nodes = 1 + + def add_options(self, parser): + parser.description = "This test will fail and exit with code 222" + parser.usage = "warnet run /path/to/scenario_buggy_failure.py" + + def run_test(self): + raise Exception("Failed execution!") + + +def main(): + Failure("").main() + + +if __name__ == "__main__": + main() diff --git a/test/data/scenario_connect_dag.py b/resources/scenarios/test_scenarios/connect_dag.py similarity index 60% rename from test/data/scenario_connect_dag.py rename to resources/scenarios/test_scenarios/connect_dag.py index f0565f9b7..c45caef6f 100644 --- a/test/data/scenario_connect_dag.py +++ b/resources/scenarios/test_scenarios/connect_dag.py @@ -2,13 +2,9 @@ import os from enum import Enum, auto, unique -from time import sleep -from warnet.test_framework_bridge import WarnetTestFramework - - -def cli_help(): - return "Connect a complete DAG from a set of unconnected nodes" +# The base class exists inside the commander container +from commander import Commander @unique @@ -17,23 +13,16 @@ class ConnectionType(Enum): DNS = auto() -class ConnectDag(WarnetTestFramework): +class ConnectDag(Commander): def set_test_params(self): # This is just a minimum self.num_nodes = 10 def add_options(self, parser): - parser.add_argument( - "--network_name", - dest="network_name", - default="warnet", - help="", - ) + parser.description = "Connect a complete DAG from a set of unconnected nodes" + parser.usage = "warnet run /path/to/scenario_connect_dag.py" def run_test(self): - while not self.warnet.network_connected(): - sleep(1) - # All permutations of a directed acyclic graph with zero, one, or two inputs/outputs # # │ Node │ In │ Out │ Con In │ Con Out │ @@ -65,53 +54,48 @@ def run_test(self): self.connect_nodes(5, 4) self.connect_nodes(5, 6) self.connect_nodes(6, 7) - - # Nodes 8 & 9 shall come pre-connected. Attempt to connect them anyway to test the handling - # of dns node addresses - self.connect_nodes(8, 9) - self.connect_nodes(9, 8) - self.sync_all() - zero_peers = self.nodes[0].getpeerinfo() - one_peers = self.nodes[1].getpeerinfo() - two_peers = self.nodes[2].getpeerinfo() - three_peers = self.nodes[3].getpeerinfo() - four_peers = self.nodes[4].getpeerinfo() - five_peers = self.nodes[5].getpeerinfo() - six_peers = self.nodes[6].getpeerinfo() - seven_peers = self.nodes[7].getpeerinfo() - eight_peers = self.nodes[8].getpeerinfo() - nine_peers = self.nodes[9].getpeerinfo() - - for tank in self.warnet.tanks: - self.log.info( - f"Tank {tank.index}: {tank.warnet.tanks[tank.index].get_dns_addr()} pod:" - f" {tank.warnet.tanks[tank.index].get_ip_addr()}" - ) - - self.assert_connection(zero_peers, 2, ConnectionType.DNS) - self.assert_connection(one_peers, 2, ConnectionType.DNS) - self.assert_connection(one_peers, 3, ConnectionType.DNS) + zero_peers = self.tanks["tank-0000"].getpeerinfo() + one_peers = self.tanks["tank-0001"].getpeerinfo() + two_peers = self.tanks["tank-0002"].getpeerinfo() + three_peers = self.tanks["tank-0003"].getpeerinfo() + four_peers = self.tanks["tank-0004"].getpeerinfo() + five_peers = self.tanks["tank-0005"].getpeerinfo() + six_peers = self.tanks["tank-0006"].getpeerinfo() + seven_peers = self.tanks["tank-0007"].getpeerinfo() + eight_peers = self.tanks["tank-0008"].getpeerinfo() + nine_peers = self.tanks["tank-0009"].getpeerinfo() + + for node in self.nodes: + self.log.info(f"Node {node.index}: tank={node.tank} ip={node.rpchost}") + + self.assert_connection(zero_peers, 2, ConnectionType.IP) + self.assert_connection(one_peers, 2, ConnectionType.IP) + self.assert_connection(one_peers, 3, ConnectionType.IP) self.assert_connection(two_peers, 0, ConnectionType.IP) self.assert_connection(two_peers, 1, ConnectionType.IP) - self.assert_connection(two_peers, 3, ConnectionType.DNS) - self.assert_connection(two_peers, 4, ConnectionType.DNS) + self.assert_connection(two_peers, 3, ConnectionType.IP) + self.assert_connection(two_peers, 4, ConnectionType.IP) self.assert_connection(three_peers, 1, ConnectionType.IP) self.assert_connection(three_peers, 2, ConnectionType.IP) - self.assert_connection(three_peers, 5, ConnectionType.DNS) + self.assert_connection(three_peers, 5, ConnectionType.IP) self.assert_connection(four_peers, 2, ConnectionType.IP) self.assert_connection(four_peers, 5, ConnectionType.IP) self.assert_connection(five_peers, 3, ConnectionType.IP) - self.assert_connection(five_peers, 4, ConnectionType.DNS) - self.assert_connection(five_peers, 6, ConnectionType.DNS) + self.assert_connection(five_peers, 4, ConnectionType.IP) + self.assert_connection(five_peers, 6, ConnectionType.IP) self.assert_connection(six_peers, 5, ConnectionType.IP) - self.assert_connection(six_peers, 7, ConnectionType.DNS) + self.assert_connection(six_peers, 7, ConnectionType.IP) self.assert_connection(seven_peers, 6, ConnectionType.IP) # Check the pre-connected nodes + # The only connection made by DNS name would be from the initial graph edges self.assert_connection(eight_peers, 9, ConnectionType.DNS) self.assert_connection(nine_peers, 8, ConnectionType.IP) + # TODO: This needs to cause the test to fail + # assert False + self.log.info( f"Successfully ran the connect_dag.py scenario using a temporary file: " f"{os.path.basename(__file__)} " @@ -120,17 +104,22 @@ def run_test(self): def assert_connection(self, connector, connectee_index, connection_type: ConnectionType): if connection_type == ConnectionType.DNS: assert any( - d.get("addr") == self.warnet.tanks[connectee_index].get_dns_addr() + # ignore the ...-service suffix + self.nodes[connectee_index].tank in d.get("addr") for d in connector - ), f"Could not find {self.options.network_name}-tank-00000{connectee_index}-service" + ), "Could not find conectee hostname" elif connection_type == ConnectionType.IP: assert any( - d.get("addr").split(":")[0] == self.warnet.tanks[connectee_index].get_ip_addr() + d.get("addr").split(":")[0] == self.nodes[connectee_index].rpchost for d in connector - ), f"Could not find Tank {connectee_index}'s ip addr" + ), "Could not find connectee ip addr" else: raise ValueError("ConnectionType must be of type DNS or IP") +def main(): + ConnectDag("").main() + + if __name__ == "__main__": - ConnectDag().main() + main() diff --git a/resources/scenarios/test_scenarios/generate_one_allnodes.py b/resources/scenarios/test_scenarios/generate_one_allnodes.py new file mode 100644 index 000000000..6d70f2bd8 --- /dev/null +++ b/resources/scenarios/test_scenarios/generate_one_allnodes.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +# The base class exists inside the commander container +try: + from commander import Commander +except Exception: + from resources.scenarios.commander import Commander + + +class GenOneAllNodes(Commander): + def set_test_params(self): + self.num_nodes = 1 + + def add_options(self, parser): + parser.description = ( + "Attempt to generate one block on every node the scenario has access to" + ) + parser.usage = "warnet run /path/to/generate_one_allnodes.py" + + def run_test(self): + for node in self.nodes: + wallet = self.ensure_miner(node) + addr = wallet.getnewaddress("bech32") + self.log.info(f"node: {node.tank}") + self.log.info(self.generatetoaddress(node, 1, addr)) + + +def main(): + GenOneAllNodes("").main() + + +if __name__ == "__main__": + main() diff --git a/resources/scenarios/test_scenarios/nothing.py b/resources/scenarios/test_scenarios/nothing.py new file mode 100644 index 000000000..35609e0d0 --- /dev/null +++ b/resources/scenarios/test_scenarios/nothing.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +from time import sleep + +# The base class exists inside the commander container +try: + from commander import Commander +except Exception: + from resources.scenarios.commander import Commander + + +class Nothing(Commander): + def set_test_params(self): + self.num_nodes = 1 + + def add_options(self, parser): + parser.description = "This test will do nothing, forever" + parser.usage = "warnet run /path/to/nothing.py" + + def run_test(self): + while True: + sleep(60) + + +def main(): + Nothing("").main() + + +if __name__ == "__main__": + main() diff --git a/test/data/scenario_p2p_interface.py b/resources/scenarios/test_scenarios/p2p_interface.py similarity index 68% rename from test/data/scenario_p2p_interface.py rename to resources/scenarios/test_scenarios/p2p_interface.py index 440fb86f7..61efa1012 100644 --- a/test/data/scenario_p2p_interface.py +++ b/resources/scenarios/test_scenarios/p2p_interface.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 from collections import defaultdict -from time import sleep -from test_framework.messages import CInv, msg_getdata -from test_framework.p2p import P2PInterface -from warnet.test_framework_bridge import WarnetTestFramework +# The base class exists inside the commander container +try: + from commander import Commander +except Exception: + from resources.scenarios.commander import Commander -def cli_help(): - return "Run P2P GETDATA test" +from test_framework.messages import CInv, msg_getdata +from test_framework.p2p import P2PInterface class P2PStoreBlock(P2PInterface): @@ -17,24 +18,22 @@ def __init__(self): self.blocks = defaultdict(int) def on_block(self, message): - message.block.calc_sha256() - self.blocks[message.block.sha256] += 1 + self.blocks[message.block.hash_int] += 1 -class GetdataTest(WarnetTestFramework): +class GetdataTest(Commander): def set_test_params(self): self.num_nodes = 1 - def run_test(self): - while not self.warnet.network_connected(): - self.log.info("Waiting for complete network connection...") - sleep(5) - self.log.info("Network connected") + def add_options(self, parser): + parser.description = "Run P2P GETDATA test" + parser.usage = "warnet run /path/to/scenario_p2p_interface.py" + def run_test(self): self.log.info("Adding the p2p connection") p2p_block_store = self.nodes[0].add_p2p_connection( - P2PStoreBlock(), dstaddr=self.warnet.tanks[0].ipv4, dstport=18444 + P2PStoreBlock(), dstaddr=self.tanks["tank-0000"].rpchost, dstport=18444 ) self.log.info("test that an invalid GETDATA doesn't prevent processing of future messages") @@ -49,8 +48,12 @@ def run_test(self): good_getdata = msg_getdata() good_getdata.inv.append(CInv(t=2, h=best_block)) p2p_block_store.send_and_ping(good_getdata) - p2p_block_store.wait_until(lambda: p2p_block_store.blocks[best_block] == 1) + p2p_block_store.wait_until(lambda: p2p_block_store.blocks[best_block] >= 1) + + +def main(): + GetdataTest("").main() if __name__ == "__main__": - GetdataTest().main() + main() diff --git a/resources/scenarios/test_scenarios/pyln_connect.py b/resources/scenarios/test_scenarios/pyln_connect.py new file mode 100644 index 000000000..40e68d37d --- /dev/null +++ b/resources/scenarios/test_scenarios/pyln_connect.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 + +from io import BytesIO + +from commander import Commander +from pyln.proto.message import Message, MessageNamespace +from pyln.proto.wire import PrivateKey, PublicKey, connect + + +class PyLNConnect(Commander): + def set_test_params(self): + self.num_nodes = 0 + + def run_test(self): + ln = self.lns["tank-0000-ln"] + ln.createrune() + uri = ln.uri() + pk, host = uri.split("@") + host, port = host.split(":") + + ls_privkey = PrivateKey( + bytes.fromhex("1111111111111111111111111111111111111111111111111111111111111111") + ) + remote_pubkey = PublicKey(bytes.fromhex(pk)) + + lc = connect(ls_privkey, remote_pubkey, host, port) + + # Send an init message, with no global features, and 0b10101010 as local + # features. + # From BOLT1: + # type: 16 (init) + # data: + # [u16:gflen] + # [gflen*byte:globalfeatures] + # [u16:flen] + # [flen*byte:features] + # [init_tlvs:tlvs] + lc.send_message(b"\x00\x10\x00\x00\x00\x01\xaa") + + # Import the BOLT#1 init message namesapce + ns = MessageNamespace( + [ + "msgtype,init,16", + "msgdata,init,gflen,u16,", + "msgdata,init,globalfeatures,byte,gflen", + "msgdata,init,flen,u16,", + "msgdata,init,features,byte,flen", + ] + ) + # read reply from peer + msg = lc.read_message() + self.log.info(f"Got message bytes: {msg.hex()}") + # interpret reply from peer + stream = BytesIO(msg) + msg = Message.read(ns, stream) + self.log.info(f"Decoded message type: {msg.messagetype} content: {msg.to_py()}") + + +def main(): + PyLNConnect("").main() + + +if __name__ == "__main__": + main() diff --git a/resources/scenarios/test_scenarios/signet_grinder.py b/resources/scenarios/test_scenarios/signet_grinder.py new file mode 100644 index 000000000..9830abea8 --- /dev/null +++ b/resources/scenarios/test_scenarios/signet_grinder.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +from commander import Commander + + +class SignetGrinder(Commander): + def set_test_params(self): + self.num_nodes = 0 + + def run_test(self): + self.generatetoaddress(self.tanks["miner"], 1, "tb1qjfplwf7a2dpjj04cx96rysqeastvycc0j50cch") + + +def main(): + SignetGrinder("").main() + + +if __name__ == "__main__": + main() diff --git a/src/warnet/scenarios/tx_flood.py b/resources/scenarios/tx_flood.py similarity index 86% rename from src/warnet/scenarios/tx_flood.py rename to resources/scenarios/tx_flood.py index e4fe3ce2f..7d46a6baa 100755 --- a/src/warnet/scenarios/tx_flood.py +++ b/resources/scenarios/tx_flood.py @@ -1,23 +1,23 @@ #!/usr/bin/env python3 + import threading from random import choice, randrange from time import sleep -from warnet.scenarios.utils import ensure_miner -from warnet.test_framework_bridge import WarnetTestFramework - - -def cli_help(): - return "Make a big transaction mess. Options: [--interval=]" +from commander import Commander -class TXFlood(WarnetTestFramework): +class TXFlood(Commander): def set_test_params(self): self.num_nodes = 1 self.addrs = [] self.threads = [] def add_options(self, parser): + parser.description = ( + "Sends random transactions between all nodes with available balance in their wallet" + ) + parser.usage = "warnet run /path/to/tx_flood.py [options]" parser.add_argument( "--interval", dest="interval", @@ -27,7 +27,7 @@ def add_options(self, parser): ) def orders(self, node): - wallet = ensure_miner(node) + wallet = self.ensure_miner(node) for address_type in ["legacy", "p2sh-segwit", "bech32", "bech32m"]: self.addrs.append(wallet.getnewaddress(address_type=address_type)) while True: @@ -67,5 +67,9 @@ def run_test(self): sleep(30) +def main(): + TXFlood("").main() + + if __name__ == "__main__": - TXFlood().main() + main() diff --git a/resources/scripts/apidocs.py b/resources/scripts/apidocs.py index 3815126a2..cca6fdce7 100755 --- a/resources/scripts/apidocs.py +++ b/resources/scripts/apidocs.py @@ -6,16 +6,17 @@ from click import Context from tabulate import tabulate -from warnet.cli.main import cli -file_path = Path(os.path.dirname(os.path.abspath(__file__))) / ".." / ".." / "docs" / "warcli.md" +from warnet.main import cli + +file_path = Path(os.path.dirname(os.path.abspath(__file__))) / ".." / ".." / "docs" / "warnet.md" doc = "" def print_cmd(cmd, super=""): global doc - doc += f"### `warcli{super} {cmd['name']}`" + "\n" + doc += f"### `warnet{super} {cmd['name']}`" + "\n" doc += cmd["help"].strip().replace("<", "\\<") + "\n" if len(cmd["params"]) > 1: doc += "\noptions:\n" @@ -25,19 +26,25 @@ def print_cmd(cmd, super=""): p["name"], p["type"]["param_type"] if p["type"]["param_type"] != "Unprocessed" else "String", "yes" if p["required"] else "", - '"' + p["default"] + '"' - if p["default"] and p["type"]["param_type"] == "String" - else Path(p["default"]).relative_to(Path.cwd()) - if p["default"] and p["type"]["param_type"] == "Path" - else p["default"], + format_default_value(p["default"], p["type"]["param_type"]), ] for p in cmd["params"] - if p["name"] != "help" + if p["name"] != "help" and p["name"] != "unknown_args" ] doc += tabulate(data, headers=headers, tablefmt="github") doc += "\n\n" +def format_default_value(default, param_type): + if default is None: + return "" + if param_type == "String": + return f'"{default}"' + if param_type == "Path": + return str(default) + return default + + with Context(cli) as ctx: info = ctx.to_info_dict() # root-level commands first diff --git a/resources/scripts/build-k8s-rpc.sh b/resources/scripts/build-k8s-rpc.sh deleted file mode 100755 index a61291291..000000000 --- a/resources/scripts/build-k8s-rpc.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash - -# Run with e.g.: -# $ DOCKER_REGISTRY=bitcoindevproject/warnet-rpc TAG=0.1 LATEST=1 ./scripts/build-k8s-rpc.sh Dockerfile_prod - -# Fail on any step -set -ex - -# Create a new builder to enable building multi-platform images -BUILDER_NAME="warnet-rpc-builder" -docker buildx create --name "$BUILDER_NAME" --use - -# Read DOCKER_REGISTRY and TAG from the environment -: "${DOCKER_REGISTRY?Need to set DOCKER_REGISTRY}" -: "${TAG?Need to set TAG}" -: "${LATEST:=0}" - -# Architectures for building -ARCHS="linux/amd64,linux/arm64" - -# Read Dockerfile from the first argument -DOCKERFILE_PATH=$1 -if [[ ! -f "$DOCKERFILE_PATH" ]]; then - echo "Dockerfile does not exist at the specified path: $DOCKERFILE_PATH" - exit 1 -fi - -# Determine the image tags -IMAGE_FULL_NAME="$DOCKER_REGISTRY:$TAG" -TAGS="--tag $IMAGE_FULL_NAME" - -# If LATEST=1, add the latest tag -if [[ "$LATEST" -eq 1 ]]; then - LATEST_TAG_IMAGE="$DOCKER_REGISTRY:latest" - TAGS="$TAGS --tag $LATEST_TAG_IMAGE" -fi - -# Use Buildx to build the image for the specified architectures and tag it accordingly -docker buildx build --platform "$ARCHS" \ - --file "$DOCKERFILE_PATH" \ - --progress=plain \ - $TAGS \ - . --push - -docker buildx rm "$BUILDER_NAME" diff --git a/resources/scripts/connect_logging.sh b/resources/scripts/connect_logging.sh deleted file mode 100755 index 20ea4a63e..000000000 --- a/resources/scripts/connect_logging.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -# NO `set -e` here so an error does not exit the script - -POD_NAME=$(kubectl get pods --namespace warnet-logging -l "app.kubernetes.io/name=grafana,app.kubernetes.io/instance=loki-grafana" -o jsonpath="{.items[0].metadata.name}") - -echo "Go to http://localhost:3000" -echo "Grafana pod name: ${POD_NAME}" - -while true; do - echo "Attempting to start Grafana port forwarding" - echo "Please wait... it might take a few minutes for all logging pods to start" - kubectl --namespace warnet-logging port-forward "${POD_NAME}" 3000 2>&1 - echo "Grafana port forwarding exited with status: $?" - sleep 5 -done; - -echo "warnet-logging port-forward exited" diff --git a/resources/scripts/deploy.sh b/resources/scripts/deploy.sh deleted file mode 100755 index f6d74da28..000000000 --- a/resources/scripts/deploy.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -set -euxo pipefail - - -if [ -z "${WAR_MANIFESTS+x}" ]; then - echo "WAR_MANIFESTS is unset. Please provide a path to warnet manifests." - exit 1 -fi - -# Function to check if warnet-rpc container is already running -check_warnet_rpc() { - if kubectl get pods --all-namespaces | grep -q "bitcoindevproject/warnet-rpc"; then - echo "warnet-rpc pod found" - exit 1 - fi -} - -# Deploy base configurations -kubectl apply -f "$WAR_MANIFESTS/namespace.yaml" -kubectl apply -f "$WAR_MANIFESTS/rbac-config.yaml" -kubectl apply -f "$WAR_MANIFESTS/warnet-rpc-service.yaml" - -# Deploy rpc server -if [ -n "${WAR_DEV+x}" ]; then # Dev mode selector - # Build image in local registry - docker build -t warnet/dev -f "$WAR_RPC/Dockerfile_dev" "$WAR_RPC" --load - if [ "$(kubectl config current-context)" = "docker-desktop" ]; then - sed "s?/mnt/src?$(pwd)?g" "$WAR_MANIFESTS/warnet-rpc-statefulset-dev.yaml" | kubectl apply -f - - else # assuming minikube - minikube image load warnet/dev - kubectl apply -f "$WAR_MANIFESTS/warnet-rpc-statefulset-dev.yaml" - fi -else - kubectl apply -f "$WAR_MANIFESTS/warnet-rpc-statefulset.yaml" -fi - -kubectl config set-context --current --namespace=warnet - -# Check for warnet-rpc container -check_warnet_rpc - -until kubectl get pod rpc-0 --namespace=warnet; do - echo "Waiting for server to find pod rpc-0..." - sleep 4 -done - -echo "⏲️ This could take a minute or so." -kubectl wait --for=condition=Ready --timeout=2m pod rpc-0 - -echo Done... diff --git a/resources/scripts/graphdocs.py b/resources/scripts/graphdocs.py deleted file mode 100755 index 8f3f80d81..000000000 --- a/resources/scripts/graphdocs.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 - -import os -import re -from pathlib import Path - -from tabulate import tabulate -from warnet.utils import load_schema - -graph_schema = load_schema() - -file_path = Path(os.path.dirname(os.path.abspath(__file__))) / ".." / ".." / "docs" / "graph.md" - -doc = "" - -doc += "### GraphML file format and headers\n" -doc += "```xml\n" -doc += '\n' - -sections = ["graph", "node", "edge"] - -for section in sections: - for name, details in graph_schema[section]["properties"].items(): - if "comment" not in details: - continue - vname = f'"{name}"' - vtype = f'"{details["type"]}"' - doc += f' \n' -doc += ' \n \n \n \n\n' -doc += "```\n\n" - -headers = ["key", "for", "type", "default", "explanation"] -data = [] -for section in sections: - data += [ - [name, section, p["type"], p.get("default", ""), p["comment"]] - for name, p in graph_schema[section]["properties"].items() - if "comment" in p - ] - -doc += tabulate(data, headers=headers, tablefmt="github") - - -with open(file_path) as file: - text = file.read() - -pattern = r"(## GraphML file specification\n)(.*\n)*?\Z" -updated_text = re.sub(pattern, rf"\1\n{doc}\n", text) - -with open(file_path, "w") as file: - file.write(updated_text) diff --git a/resources/scripts/install_logging.sh b/resources/scripts/install_logging.sh deleted file mode 100755 index 15fd1ab1e..000000000 --- a/resources/scripts/install_logging.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -e - -THIS_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -MANIFESTS_DIR=$( cd -- "$THIS_DIR/../manifests" &> /dev/null && pwd ) - -helm repo add grafana https://grafana.github.io/helm-charts -helm repo add prometheus-community https://prometheus-community.github.io/helm-charts -helm repo update - -helm upgrade --install --namespace warnet-logging --create-namespace --values "${MANIFESTS_DIR}/loki_values.yaml" loki grafana/loki --version 5.47.2 -helm upgrade --install --namespace warnet-logging promtail grafana/promtail -helm upgrade --install --namespace warnet-logging prometheus prometheus-community/kube-prometheus-stack --namespace warnet-logging --set grafana.enabled=false -helm upgrade --install --namespace warnet-logging loki-grafana grafana/grafana --values "${MANIFESTS_DIR}/grafana_values.yaml" \ No newline at end of file diff --git a/resources/scripts/k8s-log-collector.sh b/resources/scripts/k8s-log-collector.sh new file mode 100755 index 000000000..d98c5a38c --- /dev/null +++ b/resources/scripts/k8s-log-collector.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Set variables +NAMESPACE=${1:-default} +LOG_DIR="./k8s-logs" +TIMESTAMP=$(date +"%Y%m%d_%H%M%S") + +# Ensure log directory exists +mkdir -p "$LOG_DIR" + +# Collect logs using stern (includes logs from terminated pods) +echo "Collecting stern logs..." +stern "(tank|commander).*" --namespace="$NAMESPACE" --output default --since 1h --no-follow > "$LOG_DIR/${TIMESTAMP}_stern_logs" + +# Collect descriptions of all resources +echo "Collecting resource descriptions..." +kubectl describe all --namespace="$NAMESPACE" > "$LOG_DIR/${TIMESTAMP}_resource_descriptions.txt" + +# Collect events +echo "Collecting events..." +kubectl get events --namespace="$NAMESPACE" --sort-by='.metadata.creationTimestamp' > "$LOG_DIR/${TIMESTAMP}_events.txt" + +echo "Log collection complete. Logs saved in $LOG_DIR" diff --git a/resources/scripts/quick_start.sh b/resources/scripts/quick_start.sh deleted file mode 100755 index 7d473848c..000000000 --- a/resources/scripts/quick_start.sh +++ /dev/null @@ -1,136 +0,0 @@ -#!/bin/bash -set -euo pipefail - - -is_cygwin_etal() { - uname -s | grep -qE "CYGWIN|MINGW|MSYS" -} -is_wsl() { - grep -qEi "(Microsoft|WSL)" /proc/version &> /dev/null -} -if is_cygwin_etal || is_wsl; then - echo "Quick start does not support Windows" - exit 1 -fi - - -# Colors and styles -RESET='\033[0m' -BOLD='\033[1m' - -# Use colors if we can and have the color space -if command -v tput &> /dev/null; then - ncolors=$(tput colors) - if [ -n "$ncolors" ] && [ "$ncolors" -ge 8 ]; then - RESET=$(tput sgr0) - BOLD=$(tput bold) - fi -fi - -print_message() { - local color="$1" - local message="$2" - local format="${3:-}" - echo -e "${format}${color}${message}${RESET}" -} - -print_partial_message() { - local pre_message="$1" - local formatted_part="$2" - local post_message="$3" - local format="${4:-}" # Default to empty string if not provided - local color="${5:-$RESET}" - - echo -e "${color}${pre_message}${format}${formatted_part}${RESET}${color}${post_message}${RESET}" -} - -print_message "" "" "" -print_message "" " ╭───────────────────────────────────╮" "" -print_message "" " │ Welcome to the Warnet Quickstart │" "" -print_message "" " ╰───────────────────────────────────╯" "" -print_message "" "" "" -print_message "" " Let's find out if your system has what it takes to run Warnet..." "" -print_message "" "" "" - -minikube_path=$(command -v minikube || true) -if [ -n "$minikube_path" ]; then - print_partial_message " ⭐️ Found " "minikube" ": $minikube_path " "$BOLD" -else - print_partial_message " 💥 Could not find " "minikube" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://minikube.sigs.k8s.io/docs/start/" "$BOLD" - exit 127 -fi - -kubectl_path=$(command -v kubectl || true) -if [ -n "$kubectl_path" ]; then - print_partial_message " ⭐️ Found " "kubectl" ": $kubectl_path " "$BOLD" -else - print_partial_message " 💥 Could not find " "kubectl" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://kubernetes.io/docs/tasks/tools/" "$BOLD" - exit 127 -fi - -docker_path=$(command -v docker || true) -if [ -n "$docker_path" ]; then - print_partial_message " ⭐️ Found " "docker" ": $docker_path" "$BOLD" -else - print_partial_message " 💥 Could not find " "docker" ". Please follow this link to install Docker Engine..." "$BOLD" - print_message "" " https://docs.docker.com/engine/install/" "$BOLD" - exit 127 -fi - -current_user=$(whoami) -if id -nG "$current_user" | grep -qw "docker"; then - print_partial_message " ⭐️ Found " "$current_user" " in the docker group" "$BOLD" -else - print_partial_message " 💥 Could not find " "$current_user" " in the docker group. Please add it like this..." "$BOLD" - print_message "" " sudo usermod -aG docker $current_user && newgrp docker" "$BOLD" - exit 1 -fi - -helm_path=$(command -v helm || true) -if [ -n "$helm_path" ]; then - print_partial_message " ⭐️ Found " "helm" ": $helm_path" "$BOLD" -else - print_partial_message " 💥 Could not find " "helm" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://helm.sh/docs/intro/install/" "$BOLD" - exit 127 -fi - -just_path=$(command -v just || true) -if [ -n "$just_path" ]; then - print_partial_message " ⭐️ Found " "just" ": $just_path " "$BOLD" -else - print_partial_message " 💥 Could not find " "just" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://github.com/casey/just?tab=readme-ov-file#pre-built-binaries" "$BOLD" -fi - -python_path=$(command -v python3 || true) -if [ -n "$python_path" ]; then - print_partial_message " ⭐️ Found " "python3" ": $python_path " "$BOLD" -else - print_partial_message " 💥 Could not find " "python3" ". Please follow this link to install it (or use your package manager)..." "$BOLD" - print_message "" " https://www.python.org/downloads/" "$BOLD" - exit 127 -fi - -if [ -n "$VIRTUAL_ENV" ]; then - print_partial_message " ⭐️ Running in virtual environment: " "$VIRTUAL_ENV" "$BOLD" -else - print_partial_message " 💥 Not running in a virtual environment. " "Please activate a venv before proceeding." "$BOLD" - exit 127 -fi - -bpf_status=$(grep CONFIG_BPF /boot/config-"$(uname -r)" || true) -if [ -n "$bpf_status" ]; then - config_bpf=$(echo "$bpf_status" | grep CONFIG_BPF=y) - if [ "$config_bpf" = "CONFIG_BPF=y" ]; then - print_partial_message " ⭐️ Found " "BPF" ": Berkeley Packet Filters appear enabled" "$BOLD" - else - print_partial_message " 💥 Could not find " "BPF" ". Please figure out how to enable Berkeley Packet Filters in your kernel." "$BOLD" - exit 1 - fi -else - print_partial_message " 💥 Could not find " "BPF" ". Please figure out how to enable Berkeley Packet Filters in your kernel." "$BOLD" - exit 1 -fi diff --git a/resources/scripts/setup_minikube.sh b/resources/scripts/setup_minikube.sh deleted file mode 100755 index 7f1cd234e..000000000 --- a/resources/scripts/setup_minikube.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -set +x -set +v - -if [ -z "${WAR_RPC+x}" ]; then - echo "WAR_RPC is unset. Please provide a path to warnet RPC images." - exit 1 -fi - -ERROR_CODE=0 - -# Colors and styles -RESET='\033[0m' -BOLD='\033[1m' - -# Use colors if we can and have the color space -if command -v tput &> /dev/null; then - ncolors=$(tput colors) - if [ -n "$ncolors" ] && [ "$ncolors" -ge 8 ]; then - RESET=$(tput sgr0) - BOLD=$(tput bold) - fi -fi - -print_message() { - local color="$1" - local message="$2" - local format="${3:-}" - echo -e "${format}${color}${message}${RESET}" -} - -print_partial_message() { - local pre_message="$1" - local formatted_part="$2" - local post_message="$3" - local format="${4:-}" # Default to empty string if not provided - local color="${5:-$RESET}" - - echo -e "${color}${pre_message}${format}${formatted_part}${RESET}${color}${post_message}${RESET}" -} - -docker_path=$(command -v docker || true) -if [ -n "$docker_path" ]; then - print_partial_message " ⭐️ Found " "docker" ": $docker_path" "$BOLD" -else - print_partial_message " 💥 Could not find " "docker" ". Please follow this link to install Docker Engine..." "$BOLD" - print_message "" " https://docs.docker.com/engine/install/" "$BOLD" - ERROR_CODE=127 -fi - -current_user=$(whoami) -current_context=$(docker context show) -if id -nG "$current_user" | grep -qw "docker"; then - print_partial_message " ⭐️ Found " "$current_user" " in the docker group" "$BOLD" -elif [ "$current_context" == "rootless" ]; then - print_message " " "⭐️ Running Docker as rootless" "$BOLD" -elif [[ "$(uname)" == "Darwin" ]]; then - print_message " " "⭐️ Running Docker on Darwin" "$BOLD" -else - print_partial_message " 💥 Could not find " "$current_user" " in the docker group. Please add it like this..." "$BOLD" - print_message "" " sudo usermod -aG docker $current_user && newgrp docker" "$BOLD" - ERROR_CODE=1 -fi - -minikube_path=$(command -v minikube || true) -if [ -n "$minikube_path" ]; then - print_partial_message " ⭐️ Found " "minikube" ": $minikube_path " "$BOLD" -else - print_partial_message " 💥 Could not find " "minikube" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://minikube.sigs.k8s.io/docs/start/" "$BOLD" - ERROR_CODE=127 -fi - -kubectl_path=$(command -v kubectl || true) -if [ -n "$kubectl_path" ]; then - print_partial_message " ⭐️ Found " "kubectl" ": $kubectl_path " "$BOLD" -else - print_partial_message " 💥 Could not find " "kubectl" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://kubernetes.io/docs/tasks/tools/" "$BOLD" - ERROR_CODE=127 -fi - -helm_path=$(command -v helm || true) -if [ -n "$helm_path" ]; then - print_partial_message " ⭐️ Found " "helm" ": $helm_path" "$BOLD" -else - print_partial_message " 💥 Could not find " "helm" ". Please follow this link to install it..." "$BOLD" - print_message "" " https://helm.sh/docs/intro/install/" "$BOLD" - ERROR_CODE=127 -fi - -if [ $ERROR_CODE -ne 0 ]; then - print_message "" "There were errors in the setup process. Please fix them and try again." "$BOLD" - exit $ERROR_CODE -fi - -# Check minikube status -minikube delete - -# Prepare minikube start command -MINIKUBE_CMD="minikube start --driver=docker --container-runtime=containerd --mount --mount-string=\"$PWD:/mnt/src\"" - -# Check for WAR_CPU and add to command if set -if [ -n "${WAR_CPU:-}" ]; then - MINIKUBE_CMD="$MINIKUBE_CMD --cpus=$WAR_CPU" -fi - -# Check for WAR_MEM and add to command if set -if [ -n "${WAR_MEM:-}" ]; then - MINIKUBE_CMD="$MINIKUBE_CMD --memory=${WAR_MEM}m" -fi - -# Start minikube with the constructed command -eval "$MINIKUBE_CMD" - -echo -print_message "" "Next, run the following command to deploy warnet" "" -print_message "" " warcli cluster deploy" "$BOLD" -print_partial_message " After that, run " "warcli network start" " to start the network." "$BOLD" - diff --git a/resources/scripts/ssl/cert-gen.sh b/resources/scripts/ssl/cert-gen.sh new file mode 100755 index 000000000..c1370f884 --- /dev/null +++ b/resources/scripts/ssl/cert-gen.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Generate the private key using the P-256 curve +openssl ecparam -name prime256v1 -genkey -noout -out tls.key + +# Generate the self-signed certificate using the configuration file +# Expires in ten years, 2034 +openssl req -x509 -new -nodes -key tls.key -days 3650 -out tls.cert -config openssl-config.cnf diff --git a/resources/scripts/ssl/openssl-config.cnf b/resources/scripts/ssl/openssl-config.cnf new file mode 100644 index 000000000..2a7539ecf --- /dev/null +++ b/resources/scripts/ssl/openssl-config.cnf @@ -0,0 +1,28 @@ +[ req ] +distinguished_name = req_distinguished_name +req_extensions = req_ext +x509_extensions = v3_ca +prompt = no + +[ req_distinguished_name ] +O = lnd autogenerated cert +CN = warnet + +[ req_ext ] +keyUsage = critical, digitalSignature, keyEncipherment, keyCertSign +extendedKeyUsage = serverAuth +basicConstraints = critical, CA:true +subjectKeyIdentifier = hash + +[ v3_ca ] +keyUsage = critical, digitalSignature, keyEncipherment, keyCertSign +extendedKeyUsage = serverAuth +basicConstraints = critical, CA:true +subjectKeyIdentifier = hash +subjectAltName = @alt_names + +[ alt_names ] +DNS.1 = localhost +DNS.2 = *.default +IP.1 = 127.0.0.1 +IP.2 = ::1 diff --git a/resources/scripts/ssl/tls.cert b/resources/scripts/ssl/tls.cert new file mode 100644 index 000000000..4dfc97a8a --- /dev/null +++ b/resources/scripts/ssl/tls.cert @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB+DCCAZ6gAwIBAgIUSbyK/9viFWS3cLoPkmxZsW8fcH8wCgYIKoZIzj0EAwIw +MjEfMB0GA1UECgwWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2Fy +bmV0MB4XDTI1MDkwMzE1NDgzNFoXDTM1MDkwMTE1NDgzNFowMjEfMB0GA1UECgwW +bG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEPMA0GA1UEAwwGd2FybmV0MFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAENIGvS4bQr/zzUQnIqgJIYrPEdPMXVkv3yEyJRCFg +PyZTvxWUJy7AI3VKb7ubIXawYcnPBe7K1sgBAbTPz1c8sqOBkTCBjjAOBgNVHQ8B +Af8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUNhDWW7rajlA9sNGI/1Q5BDLH/rMwNwYDVR0RBDAwLoIJbG9jYWxo +b3N0ggkqLmRlZmF1bHSHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwCgYIKoZIzj0E +AwIDSAAwRQIhAOFm85wvwPZMJg0+16Sh0FkKqAuGVmllHnriWHQJ1NhuAiAfoxzE +9ooZuDwKy0Y3dP4DfJCrOlFNTHfp3abG7VQ+VQ== +-----END CERTIFICATE----- diff --git a/resources/scripts/ssl/tls.key b/resources/scripts/ssl/tls.key new file mode 100644 index 000000000..1f27900aa --- /dev/null +++ b/resources/scripts/ssl/tls.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIEKlsxGkakClpHqXbr6tqEey634Xc364DgGMJxLdiLHIoAoGCCqGSM49 +AwEHoUQDQgAENIGvS4bQr/zzUQnIqgJIYrPEdPMXVkv3yEyJRCFgPyZTvxWUJy7A +I3VKb7ubIXawYcnPBe7K1sgBAbTPz1c8sg== +-----END EC PRIVATE KEY----- diff --git a/resources/scripts/startd.sh b/resources/scripts/startd.sh deleted file mode 100755 index 84343510b..000000000 --- a/resources/scripts/startd.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -euxo pipefail - -if [ $# -eq 0 ]; then - echo "Please provide a path as an argument." - exit 1 -fi -RPC_PATH="$1" - -docker build -t warnet/dev -f "$RPC_PATH/Dockerfile_rpc_dev src/warnet/templates/rpc" --load -kubectl apply -f "$RPC_PATH/namespace.yaml" -kubectl apply -f "$RPC_PATH/rbac-config.yaml" -kubectl apply -f "$RPC_PATH/warnet-rpc-service.yaml" -sed "s?/mnt/src?$(pwd)?g" "$RPC_PATH/warnet-rpc-statefulset-dev.yaml" | kubectl apply -f - -kubectl config set-context --current --namespace=warnet - -echo waiting for rpc to come online -kubectl wait --for=condition=Ready --timeout=2m pod rpc-0 - -echo Done... diff --git a/resources/scripts/stop.sh b/resources/scripts/stop.sh deleted file mode 100755 index 7ea07cd1e..000000000 --- a/resources/scripts/stop.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -set -euxo pipefail - -# Delete namespaces -kubectl delete namespace warnet --ignore-not-found -kubectl delete namespace warnet-logging --ignore-not-found - -# Set the context to default namespace -kubectl config set-context --current --namespace=default - -# Delete minikube, if it exists -if command -v minikube &> /dev/null; then - minikube delete || true -fi diff --git a/ruff.toml b/ruff.toml index b940c5694..9f24bb64c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,6 +1,8 @@ +required-version = ">=0.11" extend-exclude = [ - "src/test_framework/*.py", + "resources/scenarios/test_framework", "resources/images/exporter/authproxy.py", + "src/test_framework/*", ] line-length = 100 indent-width = 4 diff --git a/src/test_framework/bdb.py b/src/test_framework/bdb.py deleted file mode 100644 index 41886c09f..000000000 --- a/src/test_framework/bdb.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2020-2021 The Bitcoin Core developers -# Distributed under the MIT software license, see the accompanying -# file COPYING or http://www.opensource.org/licenses/mit-license.php. -""" -Utilities for working directly with the wallet's BDB database file - -This is specific to the configuration of BDB used in this project: - - pagesize: 4096 bytes - - Outer database contains single subdatabase named 'main' - - btree - - btree leaf pages - -Each key-value pair is two entries in a btree leaf. The first is the key, the one that follows -is the value. And so on. Note that the entry data is itself not in the correct order. Instead -entry offsets are stored in the correct order and those offsets are needed to then retrieve -the data itself. - -Page format can be found in BDB source code dbinc/db_page.h -This only implements the deserialization of btree metadata pages and normal btree pages. Overflow -pages are not implemented but may be needed in the future if dealing with wallets with large -transactions. - -`db_dump -da wallet.dat` is useful to see the data in a wallet.dat BDB file -""" - -import struct - -# Important constants -PAGESIZE = 4096 -OUTER_META_PAGE = 0 -INNER_META_PAGE = 2 - -# Page type values -BTREE_INTERNAL = 3 -BTREE_LEAF = 5 -BTREE_META = 9 - -# Some magic numbers for sanity checking -BTREE_MAGIC = 0x053162 -DB_VERSION = 9 - -# Deserializes a leaf page into a dict. -# Btree internal pages have the same header, for those, return None. -# For the btree leaf pages, deserialize them and put all the data into a dict -def dump_leaf_page(data): - page_info = {} - page_header = data[0:26] - _, pgno, prev_pgno, next_pgno, entries, hf_offset, level, pg_type = struct.unpack('QIIIHHBB', page_header) - page_info['pgno'] = pgno - page_info['prev_pgno'] = prev_pgno - page_info['next_pgno'] = next_pgno - page_info['hf_offset'] = hf_offset - page_info['level'] = level - page_info['pg_type'] = pg_type - page_info['entry_offsets'] = struct.unpack('{}H'.format(entries), data[26:26 + entries * 2]) - page_info['entries'] = [] - - if pg_type == BTREE_INTERNAL: - # Skip internal pages. These are the internal nodes of the btree and don't contain anything relevant to us - return None - - assert pg_type == BTREE_LEAF, 'A non-btree leaf page has been encountered while dumping leaves' - - for i in range(0, entries): - offset = page_info['entry_offsets'][i] - entry = {'offset': offset} - page_data_header = data[offset:offset + 3] - e_len, pg_type = struct.unpack('HB', page_data_header) - entry['len'] = e_len - entry['pg_type'] = pg_type - entry['data'] = data[offset + 3:offset + 3 + e_len] - page_info['entries'].append(entry) - - return page_info - -# Deserializes a btree metadata page into a dict. -# Does a simple sanity check on the magic value, type, and version -def dump_meta_page(page): - # metadata page - # general metadata - metadata = {} - meta_page = page[0:72] - _, pgno, magic, version, pagesize, encrypt_alg, pg_type, metaflags, _, free, last_pgno, nparts, key_count, record_count, flags, uid = struct.unpack('QIIIIBBBBIIIIII20s', meta_page) - metadata['pgno'] = pgno - metadata['magic'] = magic - metadata['version'] = version - metadata['pagesize'] = pagesize - metadata['encrypt_alg'] = encrypt_alg - metadata['pg_type'] = pg_type - metadata['metaflags'] = metaflags - metadata['free'] = free - metadata['last_pgno'] = last_pgno - metadata['nparts'] = nparts - metadata['key_count'] = key_count - metadata['record_count'] = record_count - metadata['flags'] = flags - metadata['uid'] = uid.hex().encode() - - assert magic == BTREE_MAGIC, 'bdb magic does not match bdb btree magic' - assert pg_type == BTREE_META, 'Metadata page is not a btree metadata page' - assert version == DB_VERSION, 'Database too new' - - # btree metadata - btree_meta_page = page[72:512] - _, minkey, re_len, re_pad, root, _, crypto_magic, _, iv, chksum = struct.unpack('IIIII368sI12s16s20s', btree_meta_page) - metadata['minkey'] = minkey - metadata['re_len'] = re_len - metadata['re_pad'] = re_pad - metadata['root'] = root - metadata['crypto_magic'] = crypto_magic - metadata['iv'] = iv.hex().encode() - metadata['chksum'] = chksum.hex().encode() - - return metadata - -# Given the dict from dump_leaf_page, get the key-value pairs and put them into a dict -def extract_kv_pairs(page_data): - out = {} - last_key = None - for i, entry in enumerate(page_data['entries']): - # By virtue of these all being pairs, even number entries are keys, and odd are values - if i % 2 == 0: - out[entry['data']] = b'' - last_key = entry['data'] - else: - out[last_key] = entry['data'] - return out - -# Extract the key-value pairs of the BDB file given in filename -def dump_bdb_kv(filename): - # Read in the BDB file and start deserializing it - pages = [] - with open(filename, 'rb') as f: - data = f.read(PAGESIZE) - while len(data) > 0: - pages.append(data) - data = f.read(PAGESIZE) - - # Sanity check the meta pages - dump_meta_page(pages[OUTER_META_PAGE]) - dump_meta_page(pages[INNER_META_PAGE]) - - # Fetch the kv pairs from the leaf pages - kv = {} - for i in range(3, len(pages)): - info = dump_leaf_page(pages[i]) - if info is not None: - info_kv = extract_kv_pairs(info) - kv = {**kv, **info_kv} - return kv diff --git a/src/test_framework/muhash.py b/src/test_framework/muhash.py deleted file mode 100644 index 0d96114e3..000000000 --- a/src/test_framework/muhash.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright (c) 2020 Pieter Wuille -# Distributed under the MIT software license, see the accompanying -# file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Native Python MuHash3072 implementation.""" - -import hashlib -import unittest - -def rot32(v, bits): - """Rotate the 32-bit value v left by bits bits.""" - bits %= 32 # Make sure the term below does not throw an exception - return ((v << bits) & 0xffffffff) | (v >> (32 - bits)) - -def chacha20_doubleround(s): - """Apply a ChaCha20 double round to 16-element state array s. - - See https://cr.yp.to/chacha/chacha-20080128.pdf and https://tools.ietf.org/html/rfc8439 - """ - QUARTER_ROUNDS = [(0, 4, 8, 12), - (1, 5, 9, 13), - (2, 6, 10, 14), - (3, 7, 11, 15), - (0, 5, 10, 15), - (1, 6, 11, 12), - (2, 7, 8, 13), - (3, 4, 9, 14)] - - for a, b, c, d in QUARTER_ROUNDS: - s[a] = (s[a] + s[b]) & 0xffffffff - s[d] = rot32(s[d] ^ s[a], 16) - s[c] = (s[c] + s[d]) & 0xffffffff - s[b] = rot32(s[b] ^ s[c], 12) - s[a] = (s[a] + s[b]) & 0xffffffff - s[d] = rot32(s[d] ^ s[a], 8) - s[c] = (s[c] + s[d]) & 0xffffffff - s[b] = rot32(s[b] ^ s[c], 7) - -def chacha20_32_to_384(key32): - """Specialized ChaCha20 implementation with 32-byte key, 0 IV, 384-byte output.""" - # See RFC 8439 section 2.3 for chacha20 parameters - CONSTANTS = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574] - - key_bytes = [0]*8 - for i in range(8): - key_bytes[i] = int.from_bytes(key32[(4 * i):(4 * (i+1))], 'little') - - INITIALIZATION_VECTOR = [0] * 4 - init = CONSTANTS + key_bytes + INITIALIZATION_VECTOR - out = bytearray() - for counter in range(6): - init[12] = counter - s = init.copy() - for _ in range(10): - chacha20_doubleround(s) - for i in range(16): - out.extend(((s[i] + init[i]) & 0xffffffff).to_bytes(4, 'little')) - return bytes(out) - -def data_to_num3072(data): - """Hash a 32-byte array data to a 3072-bit number using 6 Chacha20 operations.""" - bytes384 = chacha20_32_to_384(data) - return int.from_bytes(bytes384, 'little') - -class MuHash3072: - """Class representing the MuHash3072 computation of a set. - - See https://cseweb.ucsd.edu/~mihir/papers/inchash.pdf and https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-May/014337.html - """ - - MODULUS = 2**3072 - 1103717 - - def __init__(self): - """Initialize for an empty set.""" - self.numerator = 1 - self.denominator = 1 - - def insert(self, data): - """Insert a byte array data in the set.""" - data_hash = hashlib.sha256(data).digest() - self.numerator = (self.numerator * data_to_num3072(data_hash)) % self.MODULUS - - def remove(self, data): - """Remove a byte array from the set.""" - data_hash = hashlib.sha256(data).digest() - self.denominator = (self.denominator * data_to_num3072(data_hash)) % self.MODULUS - - def digest(self): - """Extract the final hash. Does not modify this object.""" - val = (self.numerator * pow(self.denominator, -1, self.MODULUS)) % self.MODULUS - bytes384 = val.to_bytes(384, 'little') - return hashlib.sha256(bytes384).digest() - -class TestFrameworkMuhash(unittest.TestCase): - def test_muhash(self): - muhash = MuHash3072() - muhash.insert(b'\x00' * 32) - muhash.insert((b'\x01' + b'\x00' * 31)) - muhash.remove((b'\x02' + b'\x00' * 31)) - finalized = muhash.digest() - # This mirrors the result in the C++ MuHash3072 unit test - self.assertEqual(finalized[::-1].hex(), "10d312b100cbd32ada024a6646e40d3482fcff103668d2625f10002a607d5863") - - def test_chacha20(self): - def chacha_check(key, result): - self.assertEqual(chacha20_32_to_384(key)[:64].hex(), result) - - # Test vectors from https://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04#section-7 - # Since the nonce is hardcoded to 0 in our function we only use those vectors. - chacha_check([0]*32, "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586") - chacha_check([0]*31 + [1], "4540f05a9f1fb296d7736e7b208e3c96eb4fe1834688d2604f450952ed432d41bbe2a0b6ea7566d2a5d1e7e20d42af2c53d792b1c43fea817e9ad275ae546963") diff --git a/src/test_framework/script_util.py b/src/test_framework/script_util.py deleted file mode 100755 index 62894cc0f..000000000 --- a/src/test_framework/script_util.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2019-2022 The Bitcoin Core developers -# Distributed under the MIT software license, see the accompanying -# file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Useful Script constants and utils.""" -from test_framework.script import ( - CScript, - CScriptOp, - OP_0, - OP_CHECKMULTISIG, - OP_CHECKSIG, - OP_DUP, - OP_EQUAL, - OP_EQUALVERIFY, - OP_HASH160, - OP_RETURN, - hash160, - sha256, -) - -# To prevent a "tx-size-small" policy rule error, a transaction has to have a -# non-witness size of at least 65 bytes (MIN_STANDARD_TX_NONWITNESS_SIZE in -# src/policy/policy.h). Considering a Tx with the smallest possible single -# input (blank, empty scriptSig), and with an output omitting the scriptPubKey, -# we get to a minimum size of 60 bytes: -# -# Tx Skeleton: 4 [Version] + 1 [InCount] + 1 [OutCount] + 4 [LockTime] = 10 bytes -# Blank Input: 32 [PrevTxHash] + 4 [Index] + 1 [scriptSigLen] + 4 [SeqNo] = 41 bytes -# Output: 8 [Amount] + 1 [scriptPubKeyLen] = 9 bytes -# -# Hence, the scriptPubKey of the single output has to have a size of at -# least 5 bytes. -MIN_STANDARD_TX_NONWITNESS_SIZE = 65 -MIN_PADDING = MIN_STANDARD_TX_NONWITNESS_SIZE - 10 - 41 - 9 -assert MIN_PADDING == 5 - -# This script cannot be spent, allowing dust output values under -# standardness checks -DUMMY_MIN_OP_RETURN_SCRIPT = CScript([OP_RETURN] + ([OP_0] * (MIN_PADDING - 1))) -assert len(DUMMY_MIN_OP_RETURN_SCRIPT) == MIN_PADDING - -def key_to_p2pk_script(key): - key = check_key(key) - return CScript([key, OP_CHECKSIG]) - - -def keys_to_multisig_script(keys, *, k=None): - n = len(keys) - if k is None: # n-of-n multisig by default - k = n - assert k <= n - op_k = CScriptOp.encode_op_n(k) - op_n = CScriptOp.encode_op_n(n) - checked_keys = [check_key(key) for key in keys] - return CScript([op_k] + checked_keys + [op_n, OP_CHECKMULTISIG]) - - -def keyhash_to_p2pkh_script(hash): - assert len(hash) == 20 - return CScript([OP_DUP, OP_HASH160, hash, OP_EQUALVERIFY, OP_CHECKSIG]) - - -def scripthash_to_p2sh_script(hash): - assert len(hash) == 20 - return CScript([OP_HASH160, hash, OP_EQUAL]) - - -def key_to_p2pkh_script(key): - key = check_key(key) - return keyhash_to_p2pkh_script(hash160(key)) - - -def script_to_p2sh_script(script): - script = check_script(script) - return scripthash_to_p2sh_script(hash160(script)) - - -def key_to_p2sh_p2wpkh_script(key): - key = check_key(key) - p2shscript = CScript([OP_0, hash160(key)]) - return script_to_p2sh_script(p2shscript) - - -def program_to_witness_script(version, program): - if isinstance(program, str): - program = bytes.fromhex(program) - assert 0 <= version <= 16 - assert 2 <= len(program) <= 40 - assert version > 0 or len(program) in [20, 32] - return CScript([version, program]) - - -def script_to_p2wsh_script(script): - script = check_script(script) - return program_to_witness_script(0, sha256(script)) - - -def key_to_p2wpkh_script(key): - key = check_key(key) - return program_to_witness_script(0, hash160(key)) - - -def script_to_p2sh_p2wsh_script(script): - script = check_script(script) - p2shscript = CScript([OP_0, sha256(script)]) - return script_to_p2sh_script(p2shscript) - - -def output_key_to_p2tr_script(key): - assert len(key) == 32 - return program_to_witness_script(1, key) - - -def check_key(key): - if isinstance(key, str): - key = bytes.fromhex(key) # Assuming this is hex string - if isinstance(key, bytes) and (len(key) == 33 or len(key) == 65): - return key - assert False - - -def check_script(script): - if isinstance(script, str): - script = bytes.fromhex(script) # Assuming this is hex string - if isinstance(script, bytes) or isinstance(script, CScript): - return script - assert False diff --git a/src/warnet/admin.py b/src/warnet/admin.py new file mode 100644 index 000000000..0aa117c00 --- /dev/null +++ b/src/warnet/admin.py @@ -0,0 +1,140 @@ +import os +import sys +from pathlib import Path + +import click +import yaml +from rich import print as richprint + +from .constants import KUBECONFIG, NETWORK_DIR, WARGAMES_NAMESPACE_PREFIX +from .k8s import ( + K8sError, + get_cluster_of_current_context, + get_namespaces_by_type, + get_token_for_service_acount, + get_warnet_user_service_accounts_in_namespace, + open_kubeconfig, +) +from .namespaces import copy_namespaces_defaults, namespaces +from .network import copy_network_defaults + + +@click.group(name="admin", hidden=True) +def admin(): + """Admin commands for warnet project management""" + pass + + +admin.add_command(namespaces) + + +@admin.command() +def init(): + """Initialize a warnet project in the current directory""" + current_dir = os.getcwd() + if os.listdir(current_dir): + richprint("[yellow]Warning: Current directory is not empty[/yellow]") + if not click.confirm("Do you want to continue?", default=True): + return + + copy_network_defaults(Path(current_dir)) + copy_namespaces_defaults(Path(current_dir)) + richprint( + f"[green]Copied network and namespace example files to {Path(current_dir) / NETWORK_DIR.name}[/green]" + ) + richprint(f"[green]Created warnet project structure in {current_dir}[/green]") + + +@admin.command() +@click.option( + "--kubeconfig-dir", + default="kubeconfigs", + help="Directory to store kubeconfig files (default: kubeconfigs)", +) +@click.option( + "--token-duration", + default=172800, + type=int, + help="Duration of the token in seconds (default: 48 hours)", +) +def create_kubeconfigs(kubeconfig_dir, token_duration): + """Create kubeconfig files for ServiceAccounts""" + kubeconfig_dir = os.path.expanduser(kubeconfig_dir) + + try: + kubeconfig_data = open_kubeconfig(KUBECONFIG) + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not open auth_config: {KUBECONFIG}", fg="red") + sys.exit(1) + + cluster = get_cluster_of_current_context(kubeconfig_data) + + os.makedirs(kubeconfig_dir, exist_ok=True) + + # Get all namespaces that start with prefix + # This assumes when deploying multiple namespaces for the purpose of team games, all namespaces start with a prefix, + # e.g., tabconf-wargames-*. Currently, this is a bit brittle, but we can improve on this in the future + # by automatically applying a TEAM_PREFIX when creating the get_warnet_namespaces + # TODO: choose a prefix convention and have it managed by the helm charts instead of requiring the + # admin user to pipe through the correct string in multiple places. Another would be to use + # labels instead of namespace naming conventions + warnet_namespaces = get_namespaces_by_type(WARGAMES_NAMESPACE_PREFIX) + + for v1namespace in warnet_namespaces: + namespace = v1namespace.metadata.name + click.echo(f"Processing namespace: {namespace}") + service_accounts = get_warnet_user_service_accounts_in_namespace(namespace) + + for sa in service_accounts: + name = sa.metadata.name + # Create a token for the ServiceAccount with specified duration + try: + token = get_token_for_service_acount(sa, token_duration) + except Exception as e: + click.echo( + f"Failed to create token for ServiceAccount {name} in namespace {namespace}. Error: {str(e)}. Skipping..." + ) + continue + + # Create a kubeconfig file for the user + kubeconfig_file = os.path.join(kubeconfig_dir, f"{name}-{namespace}-kubeconfig") + + # TODO: move yaml out of python code to resources/manifests/ + # + # might not be worth it since we are just reading the yaml to then create a bunch of values and its not + # actually used to deploy anything into the cluster + # Then benefit would be making this code a bit cleaner and easy to follow, fwiw + kubeconfig_dict = { + "apiVersion": "v1", + "kind": "Config", + "clusters": [cluster], + "users": [{"name": name, "user": {"token": token}}], + "contexts": [ + { + "name": f"{name}-{namespace}", + "context": { + "cluster": cluster["name"], + "namespace": namespace, + "user": name, + }, + } + ], + "current-context": f"{name}-{namespace}", + } + + # Write to a YAML file + with open(kubeconfig_file, "w") as f: + yaml.dump(kubeconfig_dict, f, default_flow_style=False) + + click.echo(f" Created kubeconfig file for {name}: {kubeconfig_file}") + + click.echo("---") + click.echo( + f"All kubeconfig files have been created in the '{kubeconfig_dir}' directory with a duration of {token_duration} seconds." + ) + click.echo("Distribute these files to the respective users.") + click.echo( + "Users can then use by running `warnet auth ` or with kubectl by specifying the --kubeconfig flag or by setting the KUBECONFIG environment variable." + ) + click.echo(f"Note: The tokens will expire after {token_duration} seconds.") diff --git a/src/warnet/backend/kubernetes_backend.py b/src/warnet/backend/kubernetes_backend.py deleted file mode 100644 index f8821888d..000000000 --- a/src/warnet/backend/kubernetes_backend.py +++ /dev/null @@ -1,885 +0,0 @@ -import base64 -import logging -import re -import subprocess -import time -from pathlib import Path -from typing import cast - -import yaml -from kubernetes import client, config -from kubernetes.client.exceptions import ApiValueError -from kubernetes.client.models.v1_pod import V1Pod -from kubernetes.client.models.v1_service import V1Service -from kubernetes.client.rest import ApiException -from kubernetes.dynamic import DynamicClient -from kubernetes.dynamic.exceptions import NotFoundError, ResourceNotFoundError -from kubernetes.stream import stream -from warnet.cli.image import build_image -from warnet.services import SERVICES, ServiceType -from warnet.status import RunningStatus -from warnet.tank import Tank -from warnet.utils import parse_raw_messages - -DOCKER_REGISTRY_CORE = "bitcoindevproject/bitcoin" -LOCAL_REGISTRY = "warnet/bitcoin-core" - -POD_PREFIX = "tank" -BITCOIN_CONTAINER_NAME = "bitcoin" -LN_CONTAINER_NAME = "ln" -LN_CB_CONTAINER_NAME = "ln-cb" -MAIN_NAMESPACE = "warnet" -PROMETHEUS_METRICS_PORT = 9332 -LND_MOUNT_PATH = "/root/.lnd" - - -logger = logging.getLogger("k8s") - - -class KubernetesBackend: - def __init__(self, config_dir: Path, network_name: str, logs_pod="fluentd") -> None: - # assumes the warnet rpc server is always - # running inside a k8s cluster as a statefulset - config.load_incluster_config() - self.client = client.CoreV1Api() - self.dynamic_client = DynamicClient(client.ApiClient()) - self.namespace = "warnet" - self.logs_pod = logs_pod - self.network_name = network_name - self.log = logger - - def build(self) -> bool: - # TODO: just return true for now, this is so we can be running either docker or k8s as a backend - # on the same branch - return True - - def up(self, warnet) -> bool: - self.deploy_pods(warnet) - return True - - def down(self, warnet) -> bool: - """ - Bring an existing network down. - e.g. `k delete -f warnet-tanks.yaml` - """ - - for tank in warnet.tanks: - self.client.delete_namespaced_pod( - self.get_pod_name(tank.index, ServiceType.BITCOIN), self.namespace - ) - self.client.delete_namespaced_service( - self.get_service_name(tank.index, ServiceType.BITCOIN), self.namespace - ) - if tank.lnnode: - self.client.delete_namespaced_pod( - self.get_pod_name(tank.index, ServiceType.LIGHTNING), self.namespace - ) - self.client.delete_namespaced_service( - self.get_service_name(tank.index, ServiceType.LIGHTNING), self.namespace - ) - - self.remove_prometheus_service_monitors(warnet.tanks) - - for service_name in warnet.services: - try: - self.client.delete_namespaced_pod( - self.get_service_pod_name(SERVICES[service_name]["container_name_suffix"]), - self.namespace, - ) - self.client.delete_namespaced_service( - self.get_service_service_name(SERVICES[service_name]["container_name_suffix"]), - self.namespace, - ) - except Exception as e: - self.log.error(f"Could not delete service: {service_name}:\n{e}") - - return True - - def get_file(self, tank_index: int, service: ServiceType, file_path: str): - """ - Read a file from inside a container - """ - pod_name = self.get_pod_name(tank_index, service) - exec_command = ["sh", "-c", f'cat "{file_path}" | base64'] - - resp = stream( - self.client.connect_get_namespaced_pod_exec, - pod_name, - self.namespace, - command=exec_command, - stderr=True, - stdin=True, - stdout=True, - tty=False, - _preload_content=False, - container=BITCOIN_CONTAINER_NAME - if service == ServiceType.BITCOIN - else LN_CONTAINER_NAME, - ) - - base64_encoded_data = "" - while resp.is_open(): - resp.update(timeout=1) - if resp.peek_stdout(): - base64_encoded_data += resp.read_stdout() - if resp.peek_stderr(): - stderr_output = resp.read_stderr() - logger.error(f"STDERR: {stderr_output}") - raise Exception(f"Problem copying file from pod: {stderr_output}") - - decoded_bytes = base64.b64decode(base64_encoded_data) - return decoded_bytes - - def get_service_pod_name(self, suffix: str) -> str: - return f"{self.network_name}-{suffix}" - - def get_service_service_name(self, suffix: str) -> str: - return f"{self.network_name}-{suffix}-service" - - def get_pod_name(self, tank_index: int, type: ServiceType) -> str: - if type == ServiceType.LIGHTNING or type == ServiceType.CIRCUITBREAKER: - return f"{self.network_name}-{POD_PREFIX}-ln-{tank_index:06d}" - return f"{self.network_name}-{POD_PREFIX}-{tank_index:06d}" - - def get_service_name(self, tank_index: int, type: ServiceType) -> str: - return f"{self.get_pod_name(tank_index, type)}-service" - - def get_pod(self, pod_name: str) -> V1Pod | None: - try: - return cast( - V1Pod, self.client.read_namespaced_pod(name=pod_name, namespace=self.namespace) - ) - except ApiException as e: - if e.status == 404: - return None - - def get_service(self, service_name: str) -> V1Service | None: - try: - return cast( - V1Service, - self.client.read_namespaced_service(name=service_name, namespace=self.namespace), - ) - except ApiException as e: - if e.status == 404: - return None - - # We could enhance this by checking the pod status as well - # The following pod phases are available: Pending, Running, Succeeded, Failed, Unknown - # For example not able to pull image will be a phase of Pending, but the container status will be ErrImagePull - def get_status(self, tank_index: int, service: ServiceType) -> RunningStatus: - pod_name = self.get_pod_name(tank_index, service) - pod = self.get_pod(pod_name) - # Possible states: - # 1. pod not found? - # -> STOPPED - # 2. pod phase Succeeded? - # -> STOPPED - # 3. pod phase Failed? - # -> FAILED - # 4. pod phase Unknown? - # -> UNKNOWN - # Pod phase is now "Running" or "Pending" - # -> otherwise we need a bug fix, return UNKNOWN - # - # The pod is ready if all containers are ready. - # 5. Pod not ready? - # -> PENDING - # 6. Pod ready? - # -> RUNNING - # - # Note: we don't know anything about deleted pods so we can't return a status for them. - # TODO: we could use a kubernetes job to keep the result 🤔 - - if pod is None: - return RunningStatus.STOPPED - - assert pod.status, "Could not get pod status" - assert pod.status.phase, "Could not get pod status.phase" - if pod.status.phase == "Succeeded": - return RunningStatus.STOPPED - if pod.status.phase == "Failed": - return RunningStatus.FAILED - if pod.status.phase == "Unknown": - return RunningStatus.UNKNOWN - if pod.status.phase == "Pending": - return RunningStatus.PENDING - - assert pod.status.phase in ("Running", "Pending"), f"Unknown pod phase {pod.status.phase}" - - # a pod is ready if all containers are ready - ready = True - for container in pod.status.container_statuses: - if container.ready is not True: - ready = False - break - return RunningStatus.RUNNING if ready else RunningStatus.PENDING - - def exec_run(self, tank_index: int, service: ServiceType, cmd: str): - pod_name = self.get_pod_name(tank_index, service) - exec_cmd = ["/bin/sh", "-c", f"{cmd}"] - self.log.debug(f"Running {exec_cmd=:} on {tank_index=:}") - if service == ServiceType.BITCOIN: - container = BITCOIN_CONTAINER_NAME - if service == ServiceType.LIGHTNING: - container = LN_CONTAINER_NAME - if service == ServiceType.CIRCUITBREAKER: - container = LN_CB_CONTAINER_NAME - result = stream( - self.client.connect_get_namespaced_pod_exec, - pod_name, - self.namespace, - container=container, - command=exec_cmd, - stderr=True, - stdin=False, - stdout=True, - tty=False, - # Avoid preloading the content to keep JSON intact - _preload_content=False, - ) - # TODO: stream result is just a string, so there is no error code to check - # ideally, we use a method where we can check for an error code, otherwise we will - # need to check for errors in the string (meh) - # - # if result.exit_code != 0: - # raise Exception( - # f"Command failed with exit code {result.exit_code}: {result.output.decode('utf-8')}" - # ) - result.run_forever() - result = result.read_all() - return result - - def get_bitcoin_debug_log(self, tank_index: int): - pod_name = self.get_pod_name(tank_index, ServiceType.BITCOIN) - logs = self.client.read_namespaced_pod_log( - name=pod_name, - namespace=self.namespace, - container=BITCOIN_CONTAINER_NAME, - ) - return logs - - def ln_cli(self, tank: Tank, command: list[str]): - if tank.lnnode is None: - raise Exception("No LN node configured for tank") - cmd = tank.lnnode.generate_cli_command(command) - self.log.debug(f"Running lncli {cmd=:} on {tank.index=:}") - return self.exec_run(tank.index, ServiceType.LIGHTNING, cmd) - - def ln_pub_key(self, tank) -> str: - if tank.lnnode is None: - raise Exception("No LN node configured for tank") - self.log.debug(f"Getting pub key for tank {tank.index}") - return tank.lnnode.get_pub_key() - - def get_bitcoin_cli(self, tank: Tank, method: str, params=None): - if params: - cmd = f"bitcoin-cli -regtest -rpcuser={tank.rpc_user} -rpcport={tank.rpc_port} -rpcpassword={tank.rpc_password} {method} {' '.join(map(str, params))}" - else: - cmd = f"bitcoin-cli -regtest -rpcuser={tank.rpc_user} -rpcport={tank.rpc_port} -rpcpassword={tank.rpc_password} {method}" - self.log.debug(f"Running bitcoin-cli {cmd=:} on {tank.index=:}") - return self.exec_run(tank.index, ServiceType.BITCOIN, cmd) - - def get_messages( - self, - a_index: int, - b_index: int, - bitcoin_network: str = "regtest", - ): - b_pod = self.get_pod(self.get_pod_name(b_index, ServiceType.BITCOIN)) - b_service = self.get_service(self.get_service_name(b_index, ServiceType.BITCOIN)) - subdir = "/" if bitcoin_network == "main" else f"{bitcoin_network}/" - base_dir = f"/root/.bitcoin/{subdir}message_capture" - cmd = f"ls {base_dir}" - self.log.debug(f"Running {cmd=:} on {a_index=:}") - dirs = self.exec_run( - a_index, - ServiceType.BITCOIN, - cmd, - ) - dirs = dirs.splitlines() - self.log.debug(f"Got dirs: {dirs}") - messages = [] - - for dir_name in dirs: - if b_pod.status.pod_ip in dir_name or b_service.spec.cluster_ip in dir_name: - for file, outbound in [["msgs_recv.dat", False], ["msgs_sent.dat", True]]: - # Fetch the file contents from the container - file_path = f"{base_dir}/{dir_name}/{file}" - blob = self.get_file(a_index, ServiceType.BITCOIN, f"{file_path}") - # Parse the blob - json = parse_raw_messages(blob, outbound) - messages = messages + json - - messages.sort(key=lambda x: x["time"]) - return messages - - def logs_grep(self, pattern: str, network: str, k8s_timestamps=False, no_sort=False): - compiled_pattern = re.compile(pattern) - matching_logs = [] - pods = self.client.list_namespaced_pod(self.namespace) - relevant_pods = [pod for pod in pods.items if "warnet" in pod.metadata.name] - - for pod in relevant_pods: - try: - log_stream = self.client.read_namespaced_pod_log( - name=pod.metadata.name, - container=BITCOIN_CONTAINER_NAME, - namespace=self.namespace, - timestamps=k8s_timestamps, - _preload_content=False, - ) - for log_entry in log_stream: - log_entry_str = log_entry.decode("utf-8").strip() - if compiled_pattern.search(log_entry_str): - matching_logs.append((log_entry_str, pod.metadata.name)) - except ApiException as e: - print(f"Error fetching logs for pod {pod.metadata.name}: {e}") - - sorted_logs = matching_logs if no_sort else sorted(matching_logs, key=lambda x: x[0]) - # Prepend pod names - formatted_logs = [f"{pod_name}: {log}" for log, pod_name in sorted_logs] - - return "\n".join(formatted_logs) - - def generate_deployment_file(self, warnet): - """ - TODO: implement this - """ - pass - - def create_bitcoind_container(self, tank: Tank) -> client.V1Container: - self.log.debug(f"Creating bitcoind container for tank {tank.index}") - container_name = BITCOIN_CONTAINER_NAME - container_image = None - - # Prebuilt image - if tank.image: - container_image = tank.image - # On-demand built image - elif "/" and "#" in tank.version: - # We don't have docker installed on the RPC server, where this code will be run from, - # and it's currently unclear to me if having the RPC pod build images is a good idea. - # Don't support this for now in CI by disabling in the workflow. - - # This can be re-enabled by enabling in the workflow file and installing docker and - # docker-buildx on the rpc server image. - - # it's a git branch, building step is necessary - repo, branch = tank.version.split("#") - build_image( - repo, - branch, - LOCAL_REGISTRY, - branch, - tank.DEFAULT_BUILD_ARGS + tank.build_args, - arches="amd64", - ) - # Prebuilt major version - else: - container_image = f"{DOCKER_REGISTRY_CORE}:{tank.version}" - - peers = [ - self.get_service_name(dst_index, ServiceType.BITCOIN) for dst_index in tank.init_peers - ] - bitcoind_options = tank.get_bitcoin_conf(peers) - container_env = [client.V1EnvVar(name="BITCOIN_ARGS", value=bitcoind_options)] - - bitcoind_container = client.V1Container( - name=container_name, - image=container_image, - env=container_env, - liveness_probe=client.V1Probe( - failure_threshold=3, - initial_delay_seconds=5, - period_seconds=5, - timeout_seconds=1, - _exec=client.V1ExecAction(command=["pidof", "bitcoind"]), - ), - readiness_probe=client.V1Probe( - failure_threshold=1, - initial_delay_seconds=0, - period_seconds=1, - timeout_seconds=1, - tcp_socket=client.V1TCPSocketAction(port=tank.rpc_port), - ), - security_context=client.V1SecurityContext( - privileged=True, - capabilities=client.V1Capabilities(add=["NET_ADMIN", "NET_RAW"]), - ), - ) - self.log.debug( - f"Created bitcoind container for tank {tank.index} using {bitcoind_options=:}" - ) - return bitcoind_container - - def create_prometheus_container(self, tank) -> client.V1Container: - env = [ - client.V1EnvVar(name="BITCOIN_RPC_HOST", value="127.0.0.1"), - client.V1EnvVar(name="BITCOIN_RPC_PORT", value=str(tank.rpc_port)), - client.V1EnvVar(name="BITCOIN_RPC_USER", value=tank.rpc_user), - client.V1EnvVar(name="BITCOIN_RPC_PASSWORD", value=tank.rpc_password), - ] - if tank.metrics is not None: - env.append( - client.V1EnvVar(name="METRICS", value=tank.metrics), - ) - return client.V1Container( - name="prometheus", image="bitcoindevproject/bitcoin-exporter:latest", env=env - ) - - def check_logging_crds_installed(self): - logging_crd_name = "servicemonitors.monitoring.coreos.com" - api = client.ApiextensionsV1Api() - crds = api.list_custom_resource_definition() - return bool(any(crd.metadata.name == logging_crd_name for crd in crds.items)) - - def apply_prometheus_service_monitors(self, tanks): - for tank in tanks: - if not tank.exporter: - continue - - tank_name = self.get_pod_name(tank.index, ServiceType.BITCOIN) - - service_monitor = { - "apiVersion": "monitoring.coreos.com/v1", - "kind": "ServiceMonitor", - "metadata": { - "name": tank_name, - "namespace": MAIN_NAMESPACE, - "labels": { - "app.kubernetes.io/name": "bitcoind-metrics", - "release": "prometheus", - }, - }, - "spec": { - "endpoints": [{"port": "prometheus-metrics"}], - "selector": {"matchLabels": {"app": tank_name}}, - }, - } - # Create the custom resource using the dynamic client - sc_crd = self.dynamic_client.resources.get( - api_version="monitoring.coreos.com/v1", kind="ServiceMonitor" - ) - sc_crd.create(body=service_monitor, namespace=MAIN_NAMESPACE) - - # attempts to delete the service monitors whether they exist or not - def remove_prometheus_service_monitors(self, tanks): - for tank in tanks: - try: - self.dynamic_client.resources.get( - api_version="monitoring.coreos.com/v1", kind="ServiceMonitor" - ).delete( - name=f"warnet-tank-{tank.index:06d}", - namespace=MAIN_NAMESPACE, - ) - except (ResourceNotFoundError, NotFoundError): - continue - - def get_lnnode_hostname(self, index: int) -> str: - return f"{self.get_service_name(index, ServiceType.LIGHTNING)}.{self.namespace}" - - def create_ln_container(self, tank, bitcoind_service_name, volume_mounts) -> client.V1Container: - # These args are appended to the Dockerfile `ENTRYPOINT ["lnd"]` - bitcoind_rpc_host = f"{bitcoind_service_name}.{self.namespace}" - lightning_dns = self.get_lnnode_hostname(tank.index) - args = tank.lnnode.get_conf(lightning_dns, bitcoind_rpc_host) - self.log.debug(f"Creating lightning container for tank {tank.index} using {args=:}") - lightning_ready_probe = "" - if tank.lnnode.impl == "lnd": - lightning_ready_probe = "lncli --network=regtest getinfo" - elif tank.lnnode.impl == "cln": - lightning_ready_probe = "lightning-cli --network=regtest getinfo" - else: - raise Exception( - f"Lightning node implementation {tank.lnnode.impl} for tank {tank.index} not supported" - ) - lightning_container = client.V1Container( - name=LN_CONTAINER_NAME, - image=tank.lnnode.image, - args=args.split(" "), - env=[ - client.V1EnvVar(name="LN_IMPL", value=tank.lnnode.impl), - ], - readiness_probe=client.V1Probe( - failure_threshold=1, - success_threshold=3, - initial_delay_seconds=10, - period_seconds=2, - timeout_seconds=2, - _exec=client.V1ExecAction(command=["/bin/sh", "-c", lightning_ready_probe]), - ), - security_context=client.V1SecurityContext( - privileged=True, - capabilities=client.V1Capabilities(add=["NET_ADMIN", "NET_RAW"]), - ), - volume_mounts=volume_mounts, - ) - self.log.debug(f"Created lightning container for tank {tank.index}") - return lightning_container - - def create_circuitbreaker_container(self, tank, volume_mounts) -> client.V1Container: - self.log.debug(f"Creating circuitbreaker container for tank {tank.index}") - cb_container = client.V1Container( - name=LN_CB_CONTAINER_NAME, - image=tank.lnnode.cb, - args=[ - "--network=regtest", - f"--rpcserver=127.0.0.1:{tank.lnnode.rpc_port}", - f"--tlscertpath={LND_MOUNT_PATH}/tls.cert", - f"--macaroonpath={LND_MOUNT_PATH}/data/chain/bitcoin/regtest/admin.macaroon", - ], - security_context=client.V1SecurityContext( - privileged=True, - capabilities=client.V1Capabilities(add=["NET_ADMIN", "NET_RAW"]), - ), - volume_mounts=volume_mounts, - ) - self.log.debug(f"Created circuitbreaker container for tank {tank.index}") - return cb_container - - def create_pod_object( - self, - tank: Tank, - containers: list[client.V1Container], - volumes: list[client.V1Volume], - name: str, - ) -> client.V1Pod: - # Create and return a Pod object - # TODO: pass a custom namespace , e.g. different warnet sims can be deployed into diff namespaces - - return client.V1Pod( - api_version="v1", - kind="Pod", - metadata=client.V1ObjectMeta( - name=name, - namespace=self.namespace, - labels={ - "app": name, - "network": tank.warnet.network_name, - }, - ), - spec=client.V1PodSpec( - # Might need some more thinking on the pod restart policy, setting to Never for now - # This means if a node has a problem it dies - restart_policy="OnFailure", - containers=containers, - volumes=volumes, - ), - ) - - def get_tank_ipv4(self, index: int) -> str | None: - pod_name = self.get_pod_name(index, ServiceType.BITCOIN) - pod = self.get_pod(pod_name) - if pod: - return pod.status.pod_ip - else: - return None - - def get_tank_dns_addr(self, index: int) -> str | None: - service_name = self.get_service_name(index, ServiceType.BITCOIN) - try: - self.client.read_namespaced_service(name=service_name, namespace=self.namespace) - except ApiValueError as e: - self.log.info(ApiValueError(f"dns addr request for {service_name} raised {str(e)}")) - return None - return service_name - - def get_tank_ip_addr(self, index: int) -> str | None: - service_name = self.get_service_name(index, ServiceType.BITCOIN) - try: - endpoints = self.client.read_namespaced_endpoints( - name=service_name, namespace=self.namespace - ) - except ApiValueError as e: - self.log.info(f"ip addr request for {service_name} raised {str(e)}") - return None - - if len(endpoints.subsets) == 0: - raise Exception(f"{service_name}'s endpoint does not have an initial subset") - initial_subset = endpoints.subsets[0] - - if len(initial_subset.addresses) == 0: - raise Exception(f"{service_name}'s initial subset does not have an initial address") - initial_address = initial_subset.addresses[0] - - return str(initial_address.ip) - - def create_bitcoind_service(self, tank) -> client.V1Service: - service_name = self.get_service_name(tank.index, ServiceType.BITCOIN) - self.log.debug(f"Creating bitcoind service {service_name} for tank {tank.index}") - service = client.V1Service( - api_version="v1", - kind="Service", - metadata=client.V1ObjectMeta( - name=service_name, - labels={ - "app": self.get_pod_name(tank.index, ServiceType.BITCOIN), - "network": tank.warnet.network_name, - }, - ), - spec=client.V1ServiceSpec( - selector={"app": self.get_pod_name(tank.index, ServiceType.BITCOIN)}, - publish_not_ready_addresses=True, - ports=[ - client.V1ServicePort(port=18444, target_port=18444, name="p2p"), - client.V1ServicePort(port=tank.rpc_port, target_port=tank.rpc_port, name="rpc"), - client.V1ServicePort( - port=tank.zmqblockport, target_port=tank.zmqblockport, name="zmqblock" - ), - client.V1ServicePort( - port=tank.zmqtxport, target_port=tank.zmqtxport, name="zmqtx" - ), - client.V1ServicePort( - port=PROMETHEUS_METRICS_PORT, - target_port=PROMETHEUS_METRICS_PORT, - name="prometheus-metrics", - ), - ], - ), - ) - self.log.debug(f"Created bitcoind service {service_name} for tank {tank.index}") - return service - - def create_lightning_service(self, tank) -> client.V1Service: - service_name = self.get_service_name(tank.index, ServiceType.LIGHTNING) - self.log.debug(f"Creating lightning service {service_name} for tank {tank.index}") - service = client.V1Service( - api_version="v1", - kind="Service", - metadata=client.V1ObjectMeta( - name=service_name, - labels={ - "app": self.get_pod_name(tank.index, ServiceType.LIGHTNING), - "network": tank.warnet.network_name, - }, - ), - spec=client.V1ServiceSpec( - selector={"app": self.get_pod_name(tank.index, ServiceType.LIGHTNING)}, - cluster_ip="None", - ports=[ - client.V1ServicePort( - port=tank.lnnode.rpc_port, target_port=tank.lnnode.rpc_port, name="rpc" - ), - ], - publish_not_ready_addresses=True, - ), - ) - self.log.debug(f"Created lightning service {service_name} for tank {tank.index}") - return service - - def deploy_pods(self, warnet): - # TODO: this is pretty hack right now, ideally it should mirror - # a similar workflow to the docker backend: - # 1. read graph file, turn graph file into k8s resources, deploy the resources - tank_resource_files = [] - self.log.debug("Deploying pods") - for tank in warnet.tanks: - # Create and deploy bitcoind pod and service - bitcoind_container = self.create_bitcoind_container(tank) - bitcoind_pod = self.create_pod_object( - tank, [bitcoind_container], [], self.get_pod_name(tank.index, ServiceType.BITCOIN) - ) - - if tank.exporter and self.check_logging_crds_installed(): - prometheus_container = self.create_prometheus_container(tank) - bitcoind_pod.spec.containers.append(prometheus_container) - - bitcoind_service = self.create_bitcoind_service(tank) - self.client.create_namespaced_pod(namespace=self.namespace, body=bitcoind_pod) - # delete the service if it already exists, ignore 404 - try: - self.client.delete_namespaced_service( - name=bitcoind_service.metadata.name, namespace=self.namespace - ) - except ApiException as e: - if e.status != 404: - raise e - self.client.create_namespaced_service(namespace=self.namespace, body=bitcoind_service) - - # Create and deploy a lightning pod - if tank.lnnode: - conts = [] - vols = [] - volume_mounts = [] - if tank.lnnode.cb: - # Create a shared volume between containers in the pod - volume_name = f"ln-cb-data-{tank.index}" - vols.append( - client.V1Volume(name=volume_name, empty_dir=client.V1EmptyDirVolumeSource()) - ) - volume_mounts.append( - client.V1VolumeMount( - name=volume_name, - mount_path=LND_MOUNT_PATH, - ) - ) - # Add circuit breaker container - conts.append(self.create_circuitbreaker_container(tank, volume_mounts)) - # Add lightning container - conts.append( - self.create_ln_container(tank, bitcoind_service.metadata.name, volume_mounts) - ) - # Put it all together in a pod - lnd_pod = self.create_pod_object( - tank, conts, vols, self.get_pod_name(tank.index, ServiceType.LIGHTNING) - ) - self.client.create_namespaced_pod(namespace=self.namespace, body=lnd_pod) - # Create service for the pod - lightning_service = self.create_lightning_service(tank) - try: - self.client.delete_namespaced_service( - name=lightning_service.metadata.name, namespace=self.namespace - ) - except ApiException as e: - if e.status != 404: - raise e - self.client.create_namespaced_service( - namespace=self.namespace, body=lightning_service - ) - - # add metrics scraping for tanks configured to export metrics - if self.check_logging_crds_installed(): - self.apply_prometheus_service_monitors(warnet.tanks) - - for service_name in warnet.services: - try: - self.service_from_json(SERVICES[service_name]) - except Exception as e: - self.log.error(f"Error starting service: {service_name}\n{e}") - - self.log.debug("Containers and services created. Configuring IP addresses") - # now that the pods have had a second to create, - # get the ips and set them on the tanks - - # TODO: this is really hacky, should probably just update the generate_ipv4 function at some point - # by moving it into the base class - for tank in warnet.tanks: - pod_ip = None - while not pod_ip: - pod_name = self.get_pod_name(tank.index, ServiceType.BITCOIN) - pod = self.get_pod(pod_name) - if pod is None or pod.status is None or getattr(pod.status, "pod_ip", None) is None: - self.log.info("Waiting for pod response or pod IP...") - time.sleep(3) - continue - pod_ip = pod.status.pod_ip - - tank._ipv4 = pod_ip - self.log.debug(f"Tank {tank.index} created") - - with open(warnet.config_dir / "warnet-tanks.yaml", "w") as f: - for pod in tank_resource_files: - yaml.dump(pod.to_dict(), f) - f.write("---\n") # separator for multiple resources - self.log.info("Pod definitions saved to warnet-tanks.yaml") - - def wait_for_healthy_tanks(self, warnet, timeout=30): - """ - Wait for healthy status on all bitcoind nodes - """ - pass - - def service_from_json(self, obj): - env = [] - for pair in obj.get("environment", []): - name, value = pair.split("=") - env.append(client.V1EnvVar(name=name, value=value)) - volume_mounts = [] - volumes = [] - for vol in obj.get("config_files", []): - volume_name, mount_path = vol.split(":") - volume_name = volume_name.replace("/", "") - volume_mounts.append(client.V1VolumeMount(name=volume_name, mount_path=mount_path)) - volumes.append( - client.V1Volume(name=volume_name, empty_dir=client.V1EmptyDirVolumeSource()) - ) - - service_container = client.V1Container( - name=self.get_service_pod_name(obj["container_name_suffix"]), - image=obj["image"], - env=env, - security_context=client.V1SecurityContext( - privileged=True, - capabilities=client.V1Capabilities(add=["NET_ADMIN", "NET_RAW"]), - ), - volume_mounts=volume_mounts, - ) - sidecar_container = client.V1Container( - name="sidecar", - image="pinheadmz/sidecar:latest", - volume_mounts=volume_mounts, - ports=[client.V1ContainerPort(container_port=22)], - ) - service_pod = client.V1Pod( - api_version="v1", - kind="Pod", - metadata=client.V1ObjectMeta( - name=self.get_service_pod_name(obj["container_name_suffix"]), - namespace=self.namespace, - labels={ - "app": self.get_service_pod_name(obj["container_name_suffix"]), - "network": self.network_name, - }, - ), - spec=client.V1PodSpec( - restart_policy="OnFailure", - containers=[service_container, sidecar_container], - volumes=volumes, - ), - ) - - # Do not ever change this variable name. xoxo, --Zip - service_service = client.V1Service( - api_version="v1", - kind="Service", - metadata=client.V1ObjectMeta( - name=self.get_service_service_name(obj["container_name_suffix"]), - labels={ - "app": self.get_service_pod_name(obj["container_name_suffix"]), - "network": self.network_name, - }, - ), - spec=client.V1ServiceSpec( - selector={"app": self.get_service_pod_name(obj["container_name_suffix"])}, - publish_not_ready_addresses=True, - ports=[ - client.V1ServicePort(name="ssh", port=22, target_port=22), - ], - ), - ) - - self.client.create_namespaced_pod(namespace=self.namespace, body=service_pod) - self.client.create_namespaced_service(namespace=self.namespace, body=service_service) - - def write_service_config(self, source_path: str, service_name: str, destination_path: str): - obj = SERVICES[service_name] - container_name = "sidecar" - # Copy the archive from our local drive (Warnet RPC container/pod) - # to the destination service's sidecar container via ssh - self.log.info( - f"Copying local {source_path} to remote {destination_path} for {service_name}" - ) - subprocess.run( - [ - "scp", - "-o", - "StrictHostKeyChecking=accept-new", - source_path, - f"root@{self.get_service_service_name(obj['container_name_suffix'])}.{self.namespace}:/arbitrary_filename.tar", - ] - ) - self.log.info(f"Finished copying tarball for {service_name}, unpacking...") - # Unpack the archive - stream( - self.client.connect_get_namespaced_pod_exec, - self.get_service_pod_name(obj["container_name_suffix"]), - self.namespace, - container=container_name, - command=["/bin/sh", "-c", f"tar -xf /arbitrary_filename.tar -C {destination_path}"], - stderr=True, - stdin=False, - stdout=True, - tty=False, - _preload_content=False, - ) - self.log.info(f"Finished unpacking config data for {service_name} to {destination_path}") diff --git a/src/warnet/bitcoin.py b/src/warnet/bitcoin.py new file mode 100644 index 000000000..5b392e22e --- /dev/null +++ b/src/warnet/bitcoin.py @@ -0,0 +1,373 @@ +import json +import os +import re +import shlex +import sys +from datetime import datetime +from io import BytesIO +from typing import Optional + +import click +from test_framework.messages import ser_uint256 +from test_framework.p2p import MESSAGEMAP +from urllib3.exceptions import MaxRetryError + +from .constants import BITCOINCORE_CONTAINER +from .k8s import get_default_namespace_or, get_mission, pod_log +from .process import run_command + + +@click.group(name="bitcoin") +def bitcoin(): + """Control running bitcoin nodes""" + + +@bitcoin.command(context_settings={"ignore_unknown_options": True}) +@click.argument("tank", type=str) +@click.argument("method", type=str) +@click.argument("params", type=click.UNPROCESSED, nargs=-1) # get raw unprocessed arguments +@click.option("--namespace", default=None, show_default=True) +def rpc(tank: str, method: str, params: list[str], namespace: Optional[str]): + """ + Call bitcoin-cli [params] on + """ + try: + result = _rpc(tank, method, params, namespace) + except Exception as e: + print(f"{e}") + sys.exit(1) + print(result) + + +def _rpc(tank: str, method: str, params: list[str], namespace: Optional[str] = None): + namespace = get_default_namespace_or(namespace) + + if params: + # First, try to join all parameters into a single string. + full_param_str = " ".join(params) + + try: + # Heuristic: if the string looks like a JSON object/array, try to parse it. + # This handles the `signet_test` case where one large JSON argument was split + # by the shell into multiple params. + if full_param_str.strip().startswith(("[", "{")): + json.loads(full_param_str) + # SUCCESS: The params form a single, valid JSON object. + # Quote the entire reconstructed string as one argument. + param_str = shlex.quote(full_param_str) + else: + # It's not a JSON object, so it must be multiple distinct arguments. + # Raise an error to fall through to the individual quoting logic. + raise ValueError + except (json.JSONDecodeError, ValueError): + # FAILURE: The params are not one single JSON object. + # This handles the `rpc_test` case with mixed arguments. + # Quote each parameter individually to preserve them as separate arguments. + param_str = " ".join(shlex.quote(p) for p in params) + + cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method} {param_str}" + else: + # Handle commands with no parameters + cmd = f"kubectl -n {namespace} exec {tank} --container {BITCOINCORE_CONTAINER} -- bitcoin-cli {method}" + + return run_command(cmd) + + +@bitcoin.command() +@click.argument("tank", type=str, required=True) +@click.option("--namespace", default=None, show_default=True) +def debug_log(tank: str, namespace: Optional[str]): + """ + Fetch the Bitcoin Core debug log from + """ + namespace = get_default_namespace_or(namespace) + cmd = f"kubectl logs {tank} --namespace {namespace}" + try: + print(run_command(cmd)) + except Exception as e: + print(f"{e}") + + +@bitcoin.command() +@click.argument("pattern", type=str, required=True) +@click.option("--show-k8s-timestamps", is_flag=True, default=False, show_default=True) +@click.option("--no-sort", is_flag=True, default=False, show_default=True) +def grep_logs(pattern: str, show_k8s_timestamps: bool, no_sort: bool): + """ + Grep combined bitcoind logs using regex + """ + + try: + tanks = get_mission("tank") + except MaxRetryError as e: + print(f"{e}") + sys.exit(1) + + matching_logs = [] + longest_namespace_len = 0 + + for tank in tanks: + if len(tank.metadata.namespace) > longest_namespace_len: + longest_namespace_len = len(tank.metadata.namespace) + + pod_name = tank.metadata.name + logs = pod_log(pod_name, BITCOINCORE_CONTAINER) + + if logs is not False: + try: + for line in logs: + log_entry = line.decode("utf-8").rstrip() + if re.search(pattern, log_entry): + matching_logs.append((log_entry, tank.metadata.namespace, pod_name)) + except Exception as e: + print(e) + except KeyboardInterrupt: + print("Interrupted streaming log!") + + # Sort logs if needed + if not no_sort: + matching_logs.sort(key=lambda x: x[0]) + + # Print matching logs + for log_entry, namespace, pod_name in matching_logs: + try: + # Split the log entry into Kubernetes timestamp, Bitcoin timestamp, and the rest of the log + k8s_timestamp, rest = log_entry.split(" ", 1) + bitcoin_timestamp, log_message = rest.split(" ", 1) + + # Format the output based on the show_k8s_timestamps option + if show_k8s_timestamps: + print( + f"{pod_name} {namespace:<{longest_namespace_len}} {k8s_timestamp} {bitcoin_timestamp} {log_message}" + ) + else: + print( + f"{pod_name} {namespace:<{longest_namespace_len}} {bitcoin_timestamp} {log_message}" + ) + except ValueError: + # If we can't parse the timestamps, just print the original log entry + print(f"{pod_name}: {log_entry}") + + return matching_logs + + +@bitcoin.command() +@click.argument("tank_a", type=str, required=True) +@click.argument("tank_b", type=str, required=True) +@click.option("--chain", default="regtest", show_default=True) +def messages(tank_a: str, tank_b: str, chain: str): + """ + Fetch messages sent between and in [chain] + + Optionally, include a namespace like so: tank-name.namespace + """ + + def parse_name_and_namespace(tank: str) -> tuple[str, Optional[str]]: + tank_split = tank.split(".") + try: + namespace = tank_split[1] + except IndexError: + namespace = None + return tank_split[0], namespace + + tank_a_split = tank_a.split(".") + tank_b_split = tank_b.split(".") + if len(tank_a_split) > 2 or len(tank_b_split) > 2: + click.secho("Accepted formats: tank-name OR tank-name.namespace") + click.secho(f"Foramts found: {tank_a} {tank_b}") + sys.exit(1) + + tank_a, namespace_a = parse_name_and_namespace(tank_a) + tank_b, namespace_b = parse_name_and_namespace(tank_b) + + try: + namespace_a = get_default_namespace_or(namespace_a) + namespace_b = get_default_namespace_or(namespace_b) + + # Get the messages + messages = get_messages( + tank_a, tank_b, chain, namespace_a=namespace_a, namespace_b=namespace_b + ) + + if not messages: + print( + f"No messages found between {tank_a} ({namespace_a}) and {tank_b} ({namespace_b})" + ) + return + + # Process and print messages + for message in messages: + if not (message.get("time") and isinstance(message["time"], (int, float))): + continue + + timestamp = datetime.utcfromtimestamp(message["time"] / 1e6).strftime( + "%Y-%m-%d %H:%M:%S" + ) + direction = ">>>" if message.get("outbound", False) else "<<<" + msgtype = message.get("msgtype", "") + body_dict = message.get("body", {}) + + if not isinstance(body_dict, dict): + continue + + body_str = ", ".join(f"{key}: {value}" for key, value in body_dict.items()) + print(f"{timestamp} {direction} {msgtype} {body_str}") + + except Exception as e: + print(f"Error fetching messages between nodes {tank_a} and {tank_b}: {e}") + + +def get_messages(tank_a: str, tank_b: str, chain: str, namespace_a: str, namespace_b: str): + """ + Fetch messages from the message capture files + """ + subdir = "" if chain == "main" else f"{chain}/" + base_dir = f"/root/.bitcoin/{subdir}message_capture" + + # Get the IP of node_b + cmd = f"kubectl get pod {tank_b} -o jsonpath='{{.status.podIP}}' --namespace {namespace_b}" + tank_b_ip = run_command(cmd).strip() + + # Get the service IP of node_b + cmd = ( + f"kubectl get service {tank_b} -o jsonpath='{{.spec.clusterIP}}' --namespace {namespace_b}" + ) + tank_b_service_ip = run_command(cmd).strip() + + # List directories in the message capture folder + cmd = f"kubectl exec {tank_a} --namespace {namespace_a} -- ls {base_dir}" + + dirs = run_command(cmd).splitlines() + + messages = [] + + for dir_name in dirs: + if tank_b_ip in dir_name or tank_b_service_ip in dir_name: + for file, outbound in [["msgs_recv.dat", False], ["msgs_sent.dat", True]]: + file_path = f"{base_dir}/{dir_name}/{file}" + # Fetch the file contents from the container + cmd = f"kubectl exec {tank_a} --namespace {namespace_a} -- cat {file_path}" + import subprocess + + blob = subprocess.run( + cmd, shell=True, capture_output=True, executable="bash" + ).stdout + + # Parse the blob + json = parse_raw_messages(blob, outbound) + messages = messages + json + + messages.sort(key=lambda x: x["time"]) + return messages + + +# This function is a hacked-up copy of process_file() from +# Bitcoin Core contrib/message-capture/message-capture-parser.py +def parse_raw_messages(blob: bytes, outbound: bool): + TIME_SIZE = 8 + LENGTH_SIZE = 4 + MSGTYPE_SIZE = 12 + + messages = [] + offset = 0 + while True: + # Read the Header + header_len = TIME_SIZE + LENGTH_SIZE + MSGTYPE_SIZE + tmp_header_raw = blob[offset : offset + header_len] + + offset = offset + header_len + if not tmp_header_raw: + break + tmp_header = BytesIO(tmp_header_raw) + time = int.from_bytes(tmp_header.read(TIME_SIZE), "little") # type: int + msgtype = tmp_header.read(MSGTYPE_SIZE).split(b"\x00", 1)[0] # type: bytes + length = int.from_bytes(tmp_header.read(LENGTH_SIZE), "little") # type: int + + # Start converting the message to a dictionary + msg_dict = {} + msg_dict["outbound"] = outbound + msg_dict["time"] = time + msg_dict["size"] = length # "size" is less readable here, but more readable in the output + + msg_ser = BytesIO(blob[offset : offset + length]) + offset = offset + length + + # Determine message type + if msgtype not in MESSAGEMAP: + # Unrecognized message type + try: + msgtype_tmp = msgtype.decode() + if not msgtype_tmp.isprintable(): + raise UnicodeDecodeError + msg_dict["msgtype"] = msgtype_tmp + except UnicodeDecodeError: + msg_dict["msgtype"] = "UNREADABLE" + msg_dict["body"] = msg_ser.read().hex() + msg_dict["error"] = "Unrecognized message type." + messages.append(msg_dict) + # print(f"WARNING - Unrecognized message type {msgtype}", file=sys.stderr) + continue + + # Deserialize the message + msg = MESSAGEMAP[msgtype]() + msg_dict["msgtype"] = msgtype.decode() + + try: + msg.deserialize(msg_ser) + except KeyboardInterrupt: + raise + except Exception: + # Unable to deserialize message body + msg_ser.seek(0, os.SEEK_SET) + msg_dict["body"] = msg_ser.read().hex() + msg_dict["error"] = "Unable to deserialize message." + messages.append(msg_dict) + # print("WARNING - Unable to deserialize message", file=sys.stderr) + continue + + # Convert body of message into a jsonable object + if length: + msg_dict["body"] = to_jsonable(msg) + messages.append(msg_dict) + return messages + + +def to_jsonable(obj: str): + HASH_INTS = [ + "blockhash", + "block_hash", + "hash", + "hashMerkleRoot", + "hashPrevBlock", + "hashstop", + "prev_header", + "sha256", + "stop_hash", + ] + + HASH_INT_VECTORS = [ + "hashes", + "headers", + "vHave", + "vHash", + ] + + if hasattr(obj, "__dict__"): + return obj.__dict__ + elif hasattr(obj, "__slots__"): + ret = {} # type: Any + for slot in obj.__slots__: + val = getattr(obj, slot, None) + if slot in HASH_INTS and isinstance(val, int): + ret[slot] = ser_uint256(val).hex() + elif slot in HASH_INT_VECTORS and all(isinstance(a, int) for a in val): + ret[slot] = [ser_uint256(a).hex() for a in val] + else: + ret[slot] = to_jsonable(val) + return ret + elif isinstance(obj, list): + return [to_jsonable(a) for a in obj] + elif isinstance(obj, bytes): + return obj.hex() + else: + return obj diff --git a/src/warnet/cli/bitcoin.py b/src/warnet/cli/bitcoin.py deleted file mode 100644 index 0c01f7d0d..000000000 --- a/src/warnet/cli/bitcoin.py +++ /dev/null @@ -1,67 +0,0 @@ -import click - -from .rpc import rpc_call - - -@click.group(name="bitcoin") -def bitcoin(): - """Control running bitcoin nodes""" - - -@bitcoin.command(context_settings={"ignore_unknown_options": True}) -@click.argument("node", type=int) -@click.argument("method", type=str) -@click.argument("params", type=str, nargs=-1) # this will capture all remaining arguments -@click.option("--network", default="warnet", show_default=True) -def rpc(node, method, params, network): - """ - Call bitcoin-cli [params] on in [network] - """ - print( - rpc_call( - "tank_bcli", {"network": network, "node": node, "method": method, "params": params} - ) - ) - - -@bitcoin.command() -@click.argument("node", type=int, required=True) -@click.option("--network", default="warnet", show_default=True) -def debug_log(node, network): - """ - Fetch the Bitcoin Core debug log from in [network] - """ - print(rpc_call("tank_debug_log", {"node": node, "network": network})) - - -@bitcoin.command() -@click.argument("node_a", type=int, required=True) -@click.argument("node_b", type=int, required=True) -@click.option("--network", default="warnet", show_default=True) -def messages(node_a, node_b, network): - """ - Fetch messages sent between and in [network] - """ - print(rpc_call("tank_messages", {"network": network, "node_a": node_a, "node_b": node_b})) - - -@bitcoin.command() -@click.argument("pattern", type=str, required=True) -@click.option("--show-k8s-timestamps", is_flag=True, default=False, show_default=True) -@click.option("--no-sort", is_flag=True, default=False, show_default=True) -@click.option("--network", default="warnet", show_default=True) -def grep_logs(pattern, network, show_k8s_timestamps, no_sort): - """ - Grep combined logs via fluentd using regex - """ - print( - rpc_call( - "logs_grep", - { - "network": network, - "pattern": pattern, - "k8s_timestamps": show_k8s_timestamps, - "no_sort": no_sort, - }, - ) - ) diff --git a/src/warnet/cli/cluster.py b/src/warnet/cli/cluster.py deleted file mode 100644 index f9a26f230..000000000 --- a/src/warnet/cli/cluster.py +++ /dev/null @@ -1,127 +0,0 @@ -import os -import subprocess -import sys -from importlib.resources import files - -import click - -from .util import run_command - -MANIFEST_PATH = files("manifests") -RPC_PATH = files("images").joinpath("rpc") - -SCRIPTS_PATH = files("scripts") -START_SCRIPT = SCRIPTS_PATH / "start.sh" -DEPLOY_SCRIPT = SCRIPTS_PATH / "deploy.sh" -INSTALL_LOGGING_SCRIPT = SCRIPTS_PATH / "install_logging.sh" -CONNECT_LOGGING_SCRIPT = SCRIPTS_PATH / "connect_logging.sh" - - -@click.group(name="cluster", chain=True) -def cluster(): - """Start, configure and stop a warnet k8s cluster\n - \b - Supports chaining, e.g: - warcli cluster deploy-logging connect-logging - """ - pass - - -@cluster.command() -@click.option("--clean", is_flag=True, help="Remove configuration files") -def setup_minikube(clean): - """Configure a local minikube cluster""" - memory = click.prompt( - "How much RAM should we assign to the minikube cluster? (MB)", - type=int, - default=4000, - ) - cpu = click.prompt( - "How many CPUs should we assign to the minikube cluster?", type=int, default=4 - ) - env = {"WAR_MEM": str(memory), "WAR_CPU": str(cpu), "WAR_RPC": RPC_PATH} - run_command(SCRIPTS_PATH / "setup_minikube.sh", stream_output=True, env=env) - - -# TODO: Add a --dev flag to this -@cluster.command() -@click.option("--dev", is_flag=True, help="Remove configuration files") -def deploy(dev: bool): - """Deploy Warnet using the current kubectl-configured cluster""" - env = {"WAR_MANIFESTS": str(MANIFEST_PATH), "WAR_RPC": RPC_PATH} - if dev: - env["WAR_DEV"] = 1 - res = run_command(SCRIPTS_PATH / "deploy.sh", stream_output=True, env=env) - if res: - _port_start_internal() - - -@cluster.command() -def teardown(): - """Stop the warnet server and tear down the cluster""" - run_command(SCRIPTS_PATH / "stop.sh", stream_output=True) - _port_stop_internal() - - -@cluster.command() -def deploy_logging(): - """Deploy logging configurations to the cluster using helm""" - run_command(SCRIPTS_PATH / "install_logging.sh", stream_output=True) - - -@cluster.command() -def connect_logging(): - """Connect kubectl to cluster logging""" - run_command(CONNECT_LOGGING_SCRIPT, stream_output=True) - - -def is_windows(): - return sys.platform.startswith("win") - - -def run_detached_process(command): - if is_windows(): - # For Windows, use CREATE_NEW_PROCESS_GROUP and DETACHED_PROCESS - subprocess.Popen( - command, - shell=True, - stdin=None, - stdout=None, - stderr=None, - close_fds=True, - creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS, - ) - else: - # For Unix-like systems, use nohup and redirect output - command = f"nohup {command} > /dev/null 2>&1 &" - subprocess.Popen(command, shell=True, stdin=None, stdout=None, stderr=None, close_fds=True) - - print(f"Started detached process: {command}") - - -def _port_start_internal(): - command = "kubectl port-forward svc/rpc 9276:9276" - run_detached_process(command) - print( - "Port forwarding on port 9276 started in the background. Use 'warcli' (or 'kubectl') to manage the warnet." - ) - - -@cluster.command() -def port_start(): - """Port forward (runs as a detached process)""" - _port_start_internal() - - -def _port_stop_internal(): - if is_windows(): - os.system("taskkill /F /IM kubectl.exe") - else: - os.system("pkill -f 'kubectl port-forward svc/rpc 9276:9276'") - print("Port forwarding stopped.") - - -@cluster.command() -def port_stop(): - """Stop the port forwarding process""" - _port_stop_internal() diff --git a/src/warnet/cli/graph.py b/src/warnet/cli/graph.py deleted file mode 100644 index 45128d603..000000000 --- a/src/warnet/cli/graph.py +++ /dev/null @@ -1,127 +0,0 @@ -import json -from io import BytesIO -from pathlib import Path - -import click -import networkx as nx -from rich import print -from warnet.utils import DEFAULT_TAG, create_cycle_graph, validate_graph_schema - - -@click.group(name="graph") -def graph(): - """Create and validate network graphs""" - - -@graph.command() -@click.argument("number", type=int) -@click.option("--outfile", type=click.Path()) -@click.option("--version", type=str, default=DEFAULT_TAG) -@click.option("--bitcoin_conf", type=click.Path()) -@click.option("--random", is_flag=True) -def create(number: int, outfile: Path, version: str, bitcoin_conf: Path, random: bool = False): - """ - Create a cycle graph with nodes, and include 7 extra random outbounds per node. - Returns XML file as string with or without --outfile option - """ - graph = create_cycle_graph(number, version, bitcoin_conf, random) - - if outfile: - file_path = Path(outfile) - nx.write_graphml(graph, file_path, named_key_ids=True) - bio = BytesIO() - nx.write_graphml(graph, bio, named_key_ids=True) - xml_data = bio.getvalue() - print(xml_data.decode("utf-8")) - - -@graph.command() -@click.argument("infile", type=click.Path()) -@click.option("--outfile", type=click.Path()) -@click.option("--cb", type=str) -@click.option("--ln_image", type=str) -def import_json(infile: Path, outfile: Path, cb: str, ln_image: str): - """ - Create a cycle graph with nodes imported from lnd `describegraph` JSON file, - and additionally include 7 extra random outbounds per node. Include lightning - channels and their policies as well. - Returns XML file as string with or without --outfile option. - """ - with open(infile) as f: - json_graph = json.loads(f.read()) - - # Start with a connected L1 graph with the right amount of tanks - graph = create_cycle_graph( - len(json_graph["nodes"]), version=DEFAULT_TAG, bitcoin_conf=None, random_version=False - ) - - # Initialize all the tanks with basic LN node configurations - for index, n in enumerate(graph.nodes()): - graph.nodes[n]["bitcoin_config"] = f"-uacomment=tank{index:06}" - graph.nodes[n]["ln"] = "lnd" - graph.nodes[n]["ln_config"] = "--protocol.wumbo-channels" - if cb: - graph.nodes[n]["ln_cb_image"] = cb - if ln_image: - graph.nodes[n]["ln_image"] = ln_image - - # Save a map of LN pubkey -> Tank index - ln_ids = {} - for index, node in enumerate(json_graph["nodes"]): - ln_ids[node["pub_key"]] = index - - # Offset for edge IDs - # Note create_cycle_graph() creates L1 edges all with the same id "0" - L1_edges = len(graph.edges) - - # Insert LN channels - # Ensure channels are in order by channel ID like lnd describegraph output - sorted_edges = sorted(json_graph["edges"], key=lambda chan: int(chan["channel_id"])) - for ln_index, channel in enumerate(sorted_edges): - src = ln_ids[channel["node1_pub"]] - tgt = ln_ids[channel["node2_pub"]] - cap = int(channel["capacity"]) - push = cap // 2 - openp = f"--local_amt={cap} --push_amt={push}" - srcp = "" - tgtp = "" - if channel["node1_policy"]: - srcp += f" --base_fee_msat={channel['node1_policy']['fee_base_msat']}" - srcp += f" --fee_rate_ppm={channel['node1_policy']['fee_rate_milli_msat']}" - srcp += f" --time_lock_delta={max(int(channel['node1_policy']['time_lock_delta']), 18)}" - srcp += f" --min_htlc_msat={max(int(channel['node1_policy']['min_htlc']), 1)}" - srcp += f" --max_htlc_msat={push * 1000}" - if channel["node2_policy"]: - tgtp += f" --base_fee_msat={channel['node2_policy']['fee_base_msat']}" - tgtp += f" --fee_rate_ppm={channel['node2_policy']['fee_rate_milli_msat']}" - tgtp += f" --time_lock_delta={max(int(channel['node2_policy']['time_lock_delta']), 18)}" - tgtp += f" --min_htlc_msat={max(int(channel['node2_policy']['min_htlc']), 1)}" - tgtp += f" --max_htlc_msat={push * 1000}" - - graph.add_edge( - src, - tgt, - key=ln_index + L1_edges, - channel_open=openp, - source_policy=srcp, - target_policy=tgtp, - ) - - if outfile: - file_path = Path(outfile) - nx.write_graphml(graph, file_path, named_key_ids=True) - bio = BytesIO() - nx.write_graphml(graph, bio, named_key_ids=True) - xml_data = bio.getvalue() - print(xml_data.decode("utf-8")) - - -@graph.command() -@click.argument("graph", type=click.Path()) -def validate(graph: Path): - """ - Validate a against the schema. - """ - with open(graph) as f: - graph = nx.parse_graphml(f.read(), node_type=int, force_multigraph=True) - return validate_graph_schema(graph) diff --git a/src/warnet/cli/image.py b/src/warnet/cli/image.py deleted file mode 100644 index 6965838b6..000000000 --- a/src/warnet/cli/image.py +++ /dev/null @@ -1,28 +0,0 @@ -import sys - -import click - -from .image_build import build_image - - -@click.group(name="image") -def image(): - """Build a a custom Warnet Bitcoin Core image""" - - -@image.command() -@click.option("--repo", required=True, type=str) -@click.option("--commit-sha", required=True, type=str) -@click.option("--registry", required=True, type=str) -@click.option("--tag", required=True, type=str) -@click.option("--build-args", required=False, type=str) -@click.option("--arches", required=False, type=str) -@click.option("--action", required=False, type=str, default="load") -def build(repo, commit_sha, registry, tag, build_args, arches, action): - """ - Build bitcoind and bitcoin-cli from at as :. - Optionally deploy to remote registry using --action=push, otherwise image is loaded to local registry. - """ - res = build_image(repo, commit_sha, registry, tag, build_args, arches, action) - if not res: - sys.exit(1) diff --git a/src/warnet/cli/ln.py b/src/warnet/cli/ln.py deleted file mode 100644 index ade55759e..000000000 --- a/src/warnet/cli/ln.py +++ /dev/null @@ -1,39 +0,0 @@ -import click - -from .rpc import rpc_call - - -@click.group(name="ln") -def ln(): - """Control running lightning nodes""" - - -@ln.command(context_settings={"ignore_unknown_options": True}) -@click.argument("node", type=int) -@click.argument("command", type=str, required=True, nargs=-1) -@click.option("--network", default="warnet", show_default=True, type=str) -def rpc(node: int, command: tuple, network: str): - """ - Call lightning cli rpc on in [network] - """ - print( - rpc_call( - "tank_lncli", - {"network": network, "node": node, "command": command}, - ) - ) - - -@ln.command(context_settings={"ignore_unknown_options": True}) -@click.argument("node", type=int) -@click.option("--network", default="warnet", show_default=True, type=str) -def pubkey(node: int, network: str): - """ - Get lightning node pub key on in [network] - """ - print( - rpc_call( - "tank_ln_pub_key", - {"network": network, "node": node}, - ) - ) diff --git a/src/warnet/cli/main.py b/src/warnet/cli/main.py deleted file mode 100644 index 3aa7e8dce..000000000 --- a/src/warnet/cli/main.py +++ /dev/null @@ -1,99 +0,0 @@ -import os -import subprocess -from importlib.resources import files - -import click -from rich import print as richprint - -from .bitcoin import bitcoin -from .cluster import cluster -from .graph import graph -from .image import image -from .ln import ln -from .network import network -from .scenarios import scenarios - -QUICK_START_PATH = files("scripts").joinpath("quick_start.sh") - - -@click.group() -def cli(): - pass - - -cli.add_command(bitcoin) -cli.add_command(cluster) -cli.add_command(graph) -cli.add_command(image) -cli.add_command(ln) -cli.add_command(network) -cli.add_command(scenarios) - - -@cli.command(name="help") -@click.argument("commands", required=False, nargs=-1) -@click.pass_context -def help_command(ctx, commands): - """ - Display help information for the given [command] (and sub-command). - If no command is given, display help for the main CLI. - """ - if not commands: - # Display help for the main CLI - richprint(ctx.parent.get_help()) - return - - # Recurse down the subcommands, fetching the command object for each - cmd_obj = cli - for command in commands: - cmd_obj = cmd_obj.get_command(ctx, command) - if cmd_obj is None: - richprint(f'Unknown command "{command}" in {commands}') - return - ctx = click.Context(cmd_obj, info_name=command, parent=ctx) - - if cmd_obj is None: - richprint(f"Unknown command: {commands}") - return - - # Get the help info - help_info = cmd_obj.get_help(ctx).strip() - # Get rid of the duplication - help_info = help_info.replace("Usage: warcli help [COMMANDS]...", "Usage: warcli", 1) - richprint(help_info) - - -cli.add_command(help_command) - - -@cli.command() -def setup(): - """Check Warnet requirements are installed""" - try: - process = subprocess.Popen( - ["/bin/bash", str(QUICK_START_PATH)], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True, - # This preserves colours from grant's lovely script! - env=dict(os.environ, TERM="xterm-256color"), - ) - - for line in iter(process.stdout.readline, ""): - print(line, end="", flush=True) - - process.stdout.close() - return_code = process.wait() - - if return_code != 0: - print(f"Quick start script failed with return code {return_code}") - return False - return True - - except Exception as e: - print(f"An error occurred while running the quick start script: {e}") - return False - - -if __name__ == "__main__": - cli() diff --git a/src/warnet/cli/network.py b/src/warnet/cli/network.py deleted file mode 100644 index b81cbaec6..000000000 --- a/src/warnet/cli/network.py +++ /dev/null @@ -1,174 +0,0 @@ -import base64 # noqa: I001 -import json -from pathlib import Path -from importlib.resources import files - -import click -from rich import print -from rich.console import Console -from rich.table import Table - -from .rpc import rpc_call # noqa: I001 -from .util import run_command - - -DEFAULT_GRAPH_FILE = files("graphs").joinpath("default.graphml") - - -def print_repr(wn: dict) -> None: - if not isinstance(wn, dict): - print("Error, cannot print_repr of non-dict") - return - console = Console() - - # Warnet table - warnet_table = Table(show_header=True, header_style="bold") - for header in wn["warnet_headers"]: - warnet_table.add_column(header) - for row in wn["warnet"]: - warnet_table.add_row(*[str(cell) for cell in row]) - - # Tank table - tank_table = Table(show_header=True, header_style="bold") - for header in wn["tank_headers"]: - tank_table.add_column(header) - for row in wn["tanks"]: - tank_table.add_row(*[str(cell) for cell in row]) - - console.print("Warnet:") - console.print(warnet_table) - console.print("\nTanks:") - console.print(tank_table) - - -@click.group(name="network") -def network(): - """Network commands""" - - -@network.command() -@click.argument("graph_file", default=DEFAULT_GRAPH_FILE, type=click.Path()) -@click.option("--force", default=False, is_flag=True, type=bool) -@click.option("--network", default="warnet", show_default=True) -def start(graph_file: Path, force: bool, network: str): - """ - Start a warnet with topology loaded from a into [network] - """ - try: - encoded_graph_file = "" - with open(graph_file, "rb") as graph_file_buffer: - encoded_graph_file = base64.b64encode(graph_file_buffer.read()).decode("utf-8") - except Exception as e: - print(f"Error encoding graph file: {e}") - return - - result = rpc_call( - "network_from_file", - {"graph_file": encoded_graph_file, "force": force, "network": network}, - ) - assert isinstance(result, dict) - print_repr(result) - - -@network.command() -@click.option("--network", default="warnet", show_default=True) -def up(network: str): - """ - Bring up a previously-stopped warnet named [network] - """ - print(rpc_call("network_up", {"network": network})) - - -@network.command() -@click.option("--network", default="warnet", show_default=True) -def down(network: str): - """ - Bring down a running warnet named [network] - """ - - running_scenarios = rpc_call("scenarios_list_running", {}) - assert isinstance(running_scenarios, list) - if running_scenarios: - for scenario in running_scenarios: - pid = scenario.get("pid") - if pid: - try: - params = {"pid": pid} - rpc_call("scenarios_stop", params) - except Exception as e: - print( - f"Exception when stopping scenario: {scenario} with PID {scenario.pid}: {e}" - ) - print("Continuing with shutdown...") - continue - print(rpc_call("network_down", {"network": network})) - - -@network.command() -@click.option("--network", default="warnet", show_default=True) -def info(network: str): - """ - Get info about a warnet named [network] - """ - result = rpc_call("network_info", {"network": network}) - assert isinstance(result, dict), "Result is not a dict" # Make mypy happy - print_repr(result) - - -@network.command() -@click.option("--network", default="warnet", show_default=True) -def status(network: str): - """ - Get status of a warnet named [network] - """ - result = rpc_call("network_status", {"network": network}) - assert isinstance(result, list), "Result is not a list" # Make mypy happy - for tank in result: - lightning_status = "" - circuitbreaker_status = "" - if "lightning_status" in tank: - lightning_status = f"\tLightning: {tank['lightning_status']}" - if "circuitbreaker_status" in tank: - circuitbreaker_status = f"\tCircuit Breaker: {tank['circuitbreaker_status']}" - print( - f"Tank: {tank['tank_index']} \tBitcoin: {tank['bitcoin_status']}{lightning_status}{circuitbreaker_status}" - ) - - -@network.command() -@click.option("--network", default="warnet", show_default=True) -def connected(network: str): - """ - Indicate whether the all of the edges in the gaph file are connected in [network] - """ - print(rpc_call("network_connected", {"network": network})) - - -@network.command() -@click.option("--network", default="warnet", show_default=True) -@click.option("--activity", type=str) -@click.option("--exclude", type=str, default="[]") -def export(network: str, activity: str, exclude: str): - """ - Export all [network] data for a "simln" service running in a container - on the network. Optionally add JSON string [activity] to simln config. - Optionally provide a list of tank indexes to [exclude]. - Returns True on success. - """ - exclude = json.loads(exclude) - print( - rpc_call("network_export", {"network": network, "activity": activity, "exclude": exclude}) - ) - - -@network.command() -@click.option("--follow", "-f", is_flag=True, help="Follow logs") -def logs(follow: bool): - """Get Kubernetes logs from the RPC server""" - command = "kubectl logs rpc-0" - stream_output = False - if follow: - command += " --follow" - stream_output = True - - run_command(command, stream_output=stream_output) diff --git a/src/warnet/cli/rpc.py b/src/warnet/cli/rpc.py deleted file mode 100644 index 9380ede0c..000000000 --- a/src/warnet/cli/rpc.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging -import sys -from typing import Any - -import requests -from jsonrpcclient.requests import request -from jsonrpcclient.responses import Error, Ok, parse -from warnet.server import WARNET_SERVER_PORT - - -class JSONRPCException(Exception): - def __init__(self, code, message): - try: - errmsg = f"{code} {message}" - except (KeyError, TypeError): - errmsg = "" - super().__init__(errmsg) - - -def rpc_call(rpc_method, params: dict[str, Any] | tuple[Any, ...] | None): - payload = request(rpc_method, params) - url = f"http://127.0.0.1:{WARNET_SERVER_PORT}/api" - try: - response = requests.post(url, json=payload) - except ConnectionRefusedError as e: - print(f"Error connecting to {url}. Is the server running and using matching API URL?") - logging.debug(e) - return - match parse(response.json()): - case Ok(result, _): - return result - case Error(code, message, _, _): - print(f"{code}: {message}") - sys.exit(1) - # raise JSONRPCException(code, message) diff --git a/src/warnet/cli/scenarios.py b/src/warnet/cli/scenarios.py deleted file mode 100644 index fb40be6e7..000000000 --- a/src/warnet/cli/scenarios.py +++ /dev/null @@ -1,110 +0,0 @@ -import base64 -import os -import sys - -import click -from rich import print -from rich.console import Console -from rich.table import Table - -from .rpc import rpc_call - - -@click.group(name="scenarios") -def scenarios(): - """Manage scenarios on a running network""" - - -@scenarios.command() -def available(): - """ - List available scenarios in the Warnet Test Framework - """ - console = Console() - result = rpc_call("scenarios_available", None) - if not isinstance(result, list): # Make mypy happy - print(f"Error. Expected list but got {type(result)}: {result}") - sys.exit(1) - - # Create the table - table = Table(show_header=True, header_style="bold") - table.add_column("Name") - table.add_column("Description") - - for scenario in result: - table.add_row(scenario[0], scenario[1]) - console.print(table) - - -@scenarios.command(context_settings={"ignore_unknown_options": True}) -@click.argument("scenario", type=str) -@click.argument("additional_args", nargs=-1, type=click.UNPROCESSED) -@click.option("--network", default="warnet", show_default=True) -def run(scenario, network, additional_args): - """ - Run from the Warnet Test Framework on [network] with optional arguments - """ - params = { - "scenario": scenario, - "additional_args": additional_args, - "network": network, - } - print(rpc_call("scenarios_run", params)) - - -@scenarios.command(context_settings={"ignore_unknown_options": True}) -@click.argument("scenario_path", type=str) -@click.argument("additional_args", nargs=-1, type=click.UNPROCESSED) -@click.option("--name", type=str) -@click.option("--network", default="warnet", show_default=True) -def run_file(scenario_path, network, additional_args, name=""): - """ - Run from the Warnet Test Framework on [network] with optional arguments - """ - if not scenario_path.endswith(".py"): - print("Error. Currently only python scenarios are supported") - sys.exit(1) - scenario_name = name if name else os.path.splitext(os.path.basename(scenario_path))[0] - scenario_base64 = "" - with open(scenario_path, "rb") as f: - scenario_base64 = base64.b64encode(f.read()).decode("utf-8") - - params = { - "scenario_base64": scenario_base64, - "scenario_name": scenario_name, - "additional_args": additional_args, - "network": network, - } - print(rpc_call("scenarios_run_file", params)) - - -@scenarios.command() -def active(): - """ - List running scenarios "name": "pid" pairs - """ - console = Console() - result = rpc_call("scenarios_list_running", {}) - if not result: - print("No scenarios running") - return - assert isinstance(result, list) # Make mypy happy - - table = Table(show_header=True, header_style="bold") - for key in result[0].keys(): # noqa: SIM118 - table.add_column(key.capitalize()) - - for scenario in result: - table.add_row(*[str(scenario[key]) for key in scenario]) - - console.print(table) - - -@scenarios.command() -@click.argument("pid", type=int) -def stop(pid: int): - """ - Stop scenario with PID from running - """ - params = {"pid": pid} - print(rpc_call("scenarios_stop", params)) diff --git a/src/warnet/cli/util.py b/src/warnet/cli/util.py deleted file mode 100644 index 80db73cd9..000000000 --- a/src/warnet/cli/util.py +++ /dev/null @@ -1,42 +0,0 @@ -import os -import subprocess - - -def run_command(command, stream_output=False, env=None): - # Merge the current environment with the provided env - full_env = os.environ.copy() - if env: - # Convert all env values to strings (only a safeguard) - env = {k: str(v) for k, v in env.items()} - full_env.update(env) - - if stream_output: - process = subprocess.Popen( - ["/bin/bash", "-c", command], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - bufsize=1, - universal_newlines=True, - env=full_env, - ) - - for line in iter(process.stdout.readline, ""): - print(line, end="") - - process.stdout.close() - return_code = process.wait() - - if return_code != 0: - print(f"Command failed with return code {return_code}") - return False - return True - else: - result = subprocess.run( - command, shell=True, capture_output=True, text=True, executable="/bin/bash" - ) - if result.returncode != 0: - print(f"Error: {result.stderr}") - return False - print(result.stdout) - return True diff --git a/src/warnet/cln.py b/src/warnet/cln.py deleted file mode 100644 index 53ed5ffa1..000000000 --- a/src/warnet/cln.py +++ /dev/null @@ -1,198 +0,0 @@ -import io -import tarfile - -from warnet.backend.kubernetes_backend import KubernetesBackend -from warnet.services import ServiceType -from warnet.utils import exponential_backoff, generate_ipv4_addr, handle_json - -from .lnchannel import LNChannel, LNPolicy -from .lnnode import LNNode -from .status import RunningStatus - -CLN_CONFIG_BASE = " ".join( - [ - "--network=regtest", - "--database-upgrade=true", - "--bitcoin-retry-timeout=600", - "--bind-addr=0.0.0.0:9735", - "--developer", - "--dev-fast-gossip", - "--log-level=debug", - ] -) - - -class CLNNode(LNNode): - def __init__(self, warnet, tank, backend: KubernetesBackend, options): - self.warnet = warnet - self.tank = tank - self.backend = backend - self.image = options["ln_image"] - self.cb = options["cb_image"] - self.ln_config = options["ln_config"] - self.ipv4 = generate_ipv4_addr(self.warnet.subnet) - self.rpc_port = 10009 - self.impl = "cln" - - @property - def status(self) -> RunningStatus: - return super().status - - @property - def cb_status(self) -> RunningStatus: - return super().cb_status - - def get_conf(self, ln_container_name, tank_container_name) -> str: - conf = CLN_CONFIG_BASE - conf += f" --alias={self.tank.index}" - conf += f" --grpc-port={self.rpc_port}" - conf += f" --bitcoin-rpcuser={self.tank.rpc_user}" - conf += f" --bitcoin-rpcpassword={self.tank.rpc_password}" - conf += f" --bitcoin-rpcconnect={tank_container_name}" - conf += f" --bitcoin-rpcport={self.tank.rpc_port}" - conf += f" --announce-addr=dns:{ln_container_name}:9735" - return conf - - @exponential_backoff(max_retries=20, max_delay=300) - @handle_json - def lncli(self, cmd) -> dict: - cli = "lightning-cli" - cmd = f"{cli} --network=regtest {cmd}" - return self.backend.exec_run(self.tank.index, ServiceType.LIGHTNING, cmd) - - def getnewaddress(self): - return self.lncli("newaddr")["bech32"] - - def get_pub_key(self): - res = self.lncli("getinfo") - return res["id"] - - def getURI(self): - res = self.lncli("getinfo") - if len(res["address"]) < 1: - return None - return f'{res["id"]}@{res["address"][0]["address"]}:{res["address"][0]["port"]}' - - def get_wallet_balance(self) -> int: - res = self.lncli("listfunds") - return int(sum(o["amount_msat"] for o in res["outputs"]) / 1000) - - # returns the channel point in the form txid:output_index - def open_channel_to_tank(self, index: int, channel_open_data: str) -> str: - tank = self.warnet.tanks[index] - [pubkey, host] = tank.lnnode.getURI().split("@") - res = self.lncli(f"fundchannel id={pubkey} {channel_open_data}") - if "txid" not in res or "outnum" not in res: - raise ValueError(f"Error opening channel to tank: {res}") - return f"{res['txid']}:{res['outnum']}" - - def update_channel_policy(self, chan_point: str, policy: str) -> str: - return self.lncli(f"setchannel {chan_point} {policy}") - - def get_graph_nodes(self) -> list[str]: - return list(n["nodeid"] for n in self.lncli("listnodes")["nodes"]) - - def get_graph_channels(self) -> list[LNChannel]: - cln_channels = self.lncli("listchannels")["channels"] - # CLN lists channels twice, once for each direction. This finds the unique channel ids. - short_channel_ids = {chan["short_channel_id"]: chan for chan in cln_channels}.keys() - channels = [] - for short_channel_id in short_channel_ids: - nodes = [ - chans for chans in cln_channels if chans["short_channel_id"] == short_channel_id - ] - # CLN has only heard about one side of the channel - if len(nodes) == 1: - channels.append(self.lnchannel_from_json(nodes[0], None)) - continue - channels.append(self.lnchannel_from_json(nodes[0], nodes[1])) - return channels - - @staticmethod - def lnchannel_from_json(node1: object, node2: object) -> LNChannel: - if not node1: - raise ValueError("node1 can't be None") - - node2_policy = ( - LNPolicy( - min_htlc=node2["htlc_minimum_msat"], - max_htlc=node2["htlc_maximum_msat"], - base_fee_msat=node2["base_fee_millisatoshi"], - fee_rate_milli_msat=node2["fee_per_millionth"], - ) - if node2 is not None - else None - ) - - return LNChannel( - node1_pub=node1["source"], - node2_pub=node1["destination"], - capacity_msat=node1["amount_msat"], - short_chan_id=node1["short_channel_id"], - node1_policy=LNPolicy( - min_htlc=node1["htlc_minimum_msat"], - max_htlc=node1["htlc_maximum_msat"], - base_fee_msat=node1["base_fee_millisatoshi"], - fee_rate_milli_msat=node1["fee_per_millionth"], - ), - node2_policy=node2_policy, - ) - - def get_peers(self) -> list[str]: - return list(p["id"] for p in self.lncli("listpeers")["peers"]) - - def connect_to_tank(self, index): - return super().connect_to_tank(index) - - def generate_cli_command(self, command: list[str]): - network = f"--network={self.tank.warnet.bitcoin_network}" - cmd = f"{network} {' '.join(command)}" - cmd = f"lightning-cli {cmd}" - return cmd - - def export(self, config: object, tar_file): - # Retrieve the credentials - ca_cert = self.backend.get_file( - self.tank.index, - ServiceType.LIGHTNING, - "/root/.lightning/regtest/ca.pem", - ) - client_cert = self.backend.get_file( - self.tank.index, - ServiceType.LIGHTNING, - "/root/.lightning/regtest/client.pem", - ) - client_key = self.backend.get_file( - self.tank.index, - ServiceType.LIGHTNING, - "/root/.lightning/regtest/client-key.pem", - ) - name = f"ln-{self.tank.index}" - ca_cert_filename = f"{name}_ca_cert.pem" - client_cert_filename = f"{name}_client_cert.pem" - client_key_filename = f"{name}_client_key.pem" - host = self.backend.get_lnnode_hostname(self.tank.index) - - # Add the files to the in-memory tar archive - tarinfo1 = tarfile.TarInfo(name=ca_cert_filename) - tarinfo1.size = len(ca_cert) - fileobj1 = io.BytesIO(ca_cert) - tar_file.addfile(tarinfo=tarinfo1, fileobj=fileobj1) - tarinfo2 = tarfile.TarInfo(name=client_cert_filename) - tarinfo2.size = len(client_cert) - fileobj2 = io.BytesIO(client_cert) - tar_file.addfile(tarinfo=tarinfo2, fileobj=fileobj2) - tarinfo3 = tarfile.TarInfo(name=client_key_filename) - tarinfo3.size = len(client_key) - fileobj3 = io.BytesIO(client_key) - tar_file.addfile(tarinfo=tarinfo3, fileobj=fileobj3) - - config["nodes"].append( - { - "id": name, - "address": f"https://{host}:{self.rpc_port}", - "ca_cert": f"/simln/{ca_cert_filename}", - "client_cert": f"/simln/{client_cert_filename}", - "client_key": f"/simln/{client_key_filename}", - } - ) diff --git a/src/warnet/constants.py b/src/warnet/constants.py new file mode 100644 index 000000000..19c75599c --- /dev/null +++ b/src/warnet/constants.py @@ -0,0 +1,235 @@ +import os +from enum import Enum +from importlib.resources import files +from pathlib import Path + +# Constants used throughout the project +# Storing as constants for now but we might want a more sophisticated config management +# at some point. +SUPPORTED_TAGS = ["29.0", "28.1", "27.0", "26.0", "25.1", "24.2", "23.2", "22.2"] +DEFAULT_TAG = SUPPORTED_TAGS[0] +WEIGHTED_TAGS = [ + tag for index, tag in enumerate(reversed(SUPPORTED_TAGS)) for _ in range(index + 1) +] + +DEFAULT_NAMESPACE = "default" +LOGGING_NAMESPACE = "warnet-logging" +INGRESS_NAMESPACE = "ingress" +WARGAMES_NAMESPACE_PREFIX = "wargames-" +KUBE_INTERNAL_NAMESPACES = ["kube-node-lease", "kube-public", "kube-system", "kubernetes-dashboard"] +HELM_COMMAND = "helm upgrade --install" + +TANK_MISSION = "tank" +COMMANDER_MISSION = "commander" +LIGHTNING_MISSION = "lightning" + +BITCOINCORE_CONTAINER = "bitcoincore" +COMMANDER_CONTAINER = "commander" + + +class HookValue(Enum): + PRE_DEPLOY = "preDeploy" + POST_DEPLOY = "postDeploy" + PRE_NODE = "preNode" + POST_NODE = "postNode" + PRE_NETWORK = "preNetwork" + POST_NETWORK = "postNetwork" + + +class WarnetContent(Enum): + HOOK_VALUE = "hook_value" + NAMESPACE = "namespace" + ANNEX = "annex" + + +class AnnexMember(Enum): + NODE_NAME = "node_name" + + +PLUGIN_ANNEX = "annex" + +DEFAULT_IMAGE_REPO = "bitcoindevproject/bitcoin" + +# Bitcoin Core config +FORK_OBSERVER_RPCAUTH = "forkobserver:1418183465eecbd407010cf60811c6a0$d4e5f0647a63429c218da1302d7f19fe627302aeb0a71a74de55346a25d8057c" +# Fork Observer config +FORK_OBSERVER_RPC_USER = "forkobserver" +FORK_OBSERVER_RPC_PASSWORD = "tabconf2024" + +# Directories and files for non-python assets, e.g., helm charts, example scenarios, default configs +SRC_DIR = files("warnet") +RESOURCES_DIR = files("resources") +NETWORK_DIR = RESOURCES_DIR.joinpath("networks") +NAMESPACES_DIR = RESOURCES_DIR.joinpath("namespaces") +SCENARIOS_DIR = RESOURCES_DIR.joinpath("scenarios") +CHARTS_DIR = RESOURCES_DIR.joinpath("charts") +MANIFESTS_DIR = RESOURCES_DIR.joinpath("manifests") +PLUGINS_DIR = RESOURCES_DIR.joinpath("plugins") +NETWORK_FILE = "network.yaml" +DEFAULTS_FILE = "node-defaults.yaml" +NAMESPACES_FILE = "namespaces.yaml" +DEFAULTS_NAMESPACE_FILE = "namespace-defaults.yaml" + +# Helm charts +BITCOIN_CHART_LOCATION = str(CHARTS_DIR.joinpath("bitcoincore")) +FORK_OBSERVER_CHART = str(CHARTS_DIR.joinpath("fork-observer")) +COMMANDER_CHART = str(CHARTS_DIR.joinpath("commander")) +NAMESPACES_CHART_LOCATION = CHARTS_DIR.joinpath("namespaces") +FORK_OBSERVER_CHART = str(files("resources.charts").joinpath("fork-observer")) +CADDY_CHART = str(files("resources.charts").joinpath("caddy")) +CADDY_INGRESS_NAME = "caddy-ingress" + +DEFAULT_NAMESPACES = Path("two_namespaces_two_users") + +# Kubeconfig related stuffs +KUBECONFIG = os.environ.get("KUBECONFIG", os.path.expanduser("~/.kube/config")) +KUBECONFIG_UNDO = KUBECONFIG + "_warnet_undo" + +# TODO: all of this logging stuff should be a helm chart +LOGGING_CONFIG = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": { + "format": "%(asctime)s | %(levelname)-7s | %(name)-8s | %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", + }, + "detailed": { + "format": "%(asctime)s | %(levelname)-7s | [%(module)21s:%(lineno)4d] | %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", + }, + }, + "handlers": { + "stdout": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "simple", + "stream": "ext://sys.stdout", + }, + "stderr": { + "class": "logging.StreamHandler", + "level": "WARNING", + "formatter": "simple", + "stream": "ext://sys.stderr", + }, + "file": { + "class": "logging.handlers.RotatingFileHandler", + "level": "DEBUG", + "formatter": "detailed", + "filename": "warnet.log", + "maxBytes": 16000000, + "backupCount": 3, + }, + }, + "loggers": { + "root": {"level": "DEBUG", "handlers": ["stdout", "stderr", "file"]}, + "urllib3.connectionpool": {"level": "WARNING", "propagate": 1}, + "kubernetes.client.rest": {"level": "WARNING", "propagate": 1}, + "werkzeug": {"level": "WARNING", "propagate": 1}, + }, +} + +LOGGING_CRD_COMMANDS = [ + "helm repo add prometheus-community https://prometheus-community.github.io/helm-charts", + "helm repo update", + "helm upgrade --install prometheus-operator-crds prometheus-community/prometheus-operator-crds", +] + +# Helm commands for logging setup +# TODO: also lots of hardcode stuff in these helm commands, will need to fix this when moving to helm charts +LOGGING_HELM_COMMANDS = [ + "helm repo add grafana https://grafana.github.io/helm-charts", + "helm repo add prometheus-community https://prometheus-community.github.io/helm-charts", + "helm repo update", + f"helm upgrade --install --namespace warnet-logging --create-namespace --values {MANIFESTS_DIR}/loki_values.yaml loki grafana/loki --version 5.47.2", + "helm upgrade --install --namespace warnet-logging promtail grafana/promtail --create-namespace", + "helm upgrade --install --namespace warnet-logging prometheus prometheus-community/kube-prometheus-stack --namespace warnet-logging --create-namespace --set grafana.enabled=false --set prometheus.prometheusSpec.maximumStartupDurationSeconds=300", + f"helm upgrade --install grafana-dashboards {CHARTS_DIR}/grafana-dashboards --namespace warnet-logging --create-namespace", + f"helm upgrade --install --namespace warnet-logging --create-namespace loki-grafana grafana/grafana --values {MANIFESTS_DIR}/grafana_values.yaml", +] + + +INGRESS_HELM_COMMANDS = [ + "helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx", + "helm repo update", + f"helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx --namespace {INGRESS_NAMESPACE} --create-namespace --set controller.progressDeadlineSeconds=600", +] + +# Helm binary +HELM_DOWNLOAD_URL_STUB = "https://get.helm.sh/" +HELM_BINARY_NAME = "helm" +HELM_BLESSED_VERSION = "v3.16.1" +HELM_BLESSED_NAME_AND_CHECKSUMS = [ + { + "name": "helm-v3.16.1-darwin-amd64.tar.gz", + "checksum": "1b194824e36da3e3889920960a93868b541c7888c905a06757e88666cfb562c9", + }, + { + "name": "helm-v3.16.1-darwin-arm64.tar.gz", + "checksum": "405a3b13f0e194180f7b84010dfe86689d7703e80612729882ad71e2a4ef3504", + }, + { + "name": "helm-v3.16.1-linux-amd64.tar.gz", + "checksum": "e57e826410269d72be3113333dbfaac0d8dfdd1b0cc4e9cb08bdf97722731ca9", + }, + { + "name": "helm-v3.16.1-linux-arm.tar.gz", + "checksum": "a15a8ddfc373628b13cd2a987206756004091a1f6a91c3b9ee8de6f0b1e2ce90", + }, + { + "name": "helm-v3.16.1-linux-arm64.tar.gz", + "checksum": "780b5b86f0db5546769b3e9f0204713bbdd2f6696dfdaac122fbe7f2f31541d2", + }, + { + "name": "helm-v3.16.1-linux-386.tar.gz", + "checksum": "92d7a47a90734b50528ffffc99cd1b2d4b9fc0f4291bac92c87ef03406a5a7b2", + }, + { + "name": "helm-v3.16.1-linux-ppc64le.tar.gz", + "checksum": "9f0178957c94516eff9a3897778edb93d78fab1f76751bd282883f584ea81c23", + }, + { + "name": "helm-v3.16.1-linux-s390x.tar.gz", + "checksum": "357f8b441cc535240f1b0ba30a42b44571d4c303dab004c9e013697b97160360", + }, + { + "name": "helm-v3.16.1-linux-riscv64.tar.gz", + "checksum": "9a2cab45b7d9282e9be7b42f86d8034dcaa2e81ab338642884843676c2f6929f", + }, + { + "name": "helm-v3.16.1-windows-amd64.zip", + "checksum": "89952ea1bace0a9498053606296ea03cf743c48294969dfc731e7f78d1dc809a", + }, + { + "name": "helm-v3.16.1-windows-arm64.zip", + "checksum": "fc370a291ed926da5e77acf42006de48e7fd5ff94d20c3f6aa10c04fea66e53c", + }, +] + + +# Kubectl binary +KUBECTL_BINARY_NAME = "kubectl" +KUBECTL_BLESSED_VERSION = "v1.31.1" +KUBECTL_DOWNLOAD_URL_STUB = f"https://dl.k8s.io/release/{KUBECTL_BLESSED_VERSION}/bin" +KUBECTL_BLESSED_NAME_AND_CHECKSUMS = [ + { + "system": "linux", + "arch": "amd64", + "checksum": "57b514a7facce4ee62c93b8dc21fda8cf62ef3fed22e44ffc9d167eab843b2ae", + }, + { + "system": "linux", + "arch": "arm64", + "checksum": "3af2451191e27ecd4ac46bb7f945f76b71e934d54604ca3ffc7fe6f5dd123edb", + }, + { + "system": "darwin", + "arch": "amd64", + "checksum": "4b86d3fb8dee8dd61f341572f1ba13c1030d493f4dc1b4831476f61f3cbb77d0", + }, + { + "system": "darwin", + "arch": "arm64", + "checksum": "08909b92e62004f4f1222dfd39214085383ea368bdd15c762939469c23484634", + }, +] diff --git a/src/warnet/control.py b/src/warnet/control.py new file mode 100644 index 000000000..d3183c73a --- /dev/null +++ b/src/warnet/control.py @@ -0,0 +1,575 @@ +import io +import json +import os +import subprocess +import sys +import time +import zipapp +from concurrent.futures import ThreadPoolExecutor, as_completed +from multiprocessing import Pool +from pathlib import Path +from typing import Optional + +import click +import inquirer +from inquirer.themes import GreenPassion +from kubernetes.client.models import V1Pod +from rich import print +from rich.console import Console +from rich.prompt import Confirm, Prompt +from rich.table import Table + +from .constants import ( + BITCOINCORE_CONTAINER, + COMMANDER_CHART, + COMMANDER_CONTAINER, + COMMANDER_MISSION, + TANK_MISSION, +) +from .k8s import ( + can_delete_pods, + delete_pod, + get_default_namespace, + get_default_namespace_or, + get_mission, + get_namespaces, + get_pod, + get_pods, + pod_log, + snapshot_bitcoin_datadir, + wait_for_init, + wait_for_pod, + write_file_to_container, +) +from .process import run_command, stream_command + +console = Console() + + +@click.command() +@click.argument("scenario_name", required=False) +def stop(scenario_name): + """Stop a running scenario or all scenarios""" + active_scenarios = [sc.metadata.name for sc in get_mission("commander")] + + if not active_scenarios: + console.print("[bold red]No active scenarios found.[/bold red]") + return + + if not scenario_name: + table = Table(title="Active Scenarios", show_header=True, header_style="bold magenta") + table.add_column("Number", style="cyan", justify="right") + table.add_column("Scenario Name", style="green") + + for idx, name in enumerate(active_scenarios, 1): + table.add_row(str(idx), name) + + console.print(table) + + choices = [str(i) for i in range(1, len(active_scenarios) + 1)] + ["a", "q"] + choice = Prompt.ask( + "[bold yellow]Enter the number of the scenario to stop, 'a' to stop all, or 'q' to quit[/bold yellow]", + choices=choices, + show_choices=False, + ) + + if choice == "q": + console.print("[bold blue]Operation cancelled.[/bold blue]") + return + elif choice == "a": + if Confirm.ask("[bold red]Are you sure you want to stop all scenarios?[/bold red]"): + stop_all_scenarios(active_scenarios) + else: + console.print("[bold blue]Operation cancelled.[/bold blue]") + return + + scenario_name = active_scenarios[int(choice) - 1] + + if scenario_name not in active_scenarios: + console.print(f"[bold red]No active scenario found with name: {scenario_name}[/bold red]") + return + + stop_scenario(scenario_name) + + +def stop_scenario(scenario_name): + """Stop a single scenario using Helm""" + # Stop the pod immediately (faster than uninstalling) + namespace = get_default_namespace() + cmd = f"kubectl --namespace {namespace} delete pod {scenario_name} --grace-period=0 --force" + if stream_command(cmd): + console.print(f"[bold green]Successfully stopped scenario: {scenario_name}[/bold green]") + else: + console.print(f"[bold red]Failed to stop scenario: {scenario_name}[/bold red]") + + # Then uninstall via helm (non-blocking) + command = f"helm uninstall {scenario_name} --namespace {namespace} --wait=false" + + # Run the helm uninstall command in the background + subprocess.Popen(command, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + console.print( + f"[bold yellow]Initiated helm uninstall for release: {scenario_name}[/bold yellow]" + ) + + +def _stop_single(scenario: str) -> str: + """ + Stop a single scenario + + Args: + scenario: Name of the scenario to stop + + Returns: + str: Message indicating the scenario has been stopped + """ + stop_scenario(scenario) + return f"Stopped scenario: {scenario}" + + +def stop_all_scenarios(scenarios) -> None: + """ + Stop all active scenarios in parallel using multiprocessing + + Args: + scenarios: List of scenario names to stop + + Returns: + None + """ + + with console.status("[bold yellow]Stopping all scenarios...[/bold yellow]"), Pool() as pool: + results = pool.map(_stop_single, scenarios) + + for result in results: + console.print(f"[bold green]{result}[/bold green]") + + console.print("[bold green]All scenarios have been stopped.[/bold green]") + + +@click.command() +def down(): + """Bring down a running warnet quickly""" + + def uninstall_release(namespace, release_name): + cmd = f"helm uninstall {release_name} --namespace {namespace} --wait" + print(f"Initiating uninstall of {release_name} in namespace {namespace}") + subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return f"Uninstalled {release_name} in namespace {namespace}" + + def delete_pod(pod_name, namespace): + cmd = f"kubectl delete pod --ignore-not-found=true {pod_name} -n {namespace} --grace-period=0 --force" + subprocess.Popen(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return f"Initiated deletion of pod: {pod_name} in namespace {namespace}" + + if not can_delete_pods(): + click.secho("You do not have permission to bring down the network.", fg="red") + return + + namespaces = get_namespaces() + release_list: list[dict[str, str]] = [] + for v1namespace in namespaces: + namespace = v1namespace.metadata.name + command = f"helm list --namespace {namespace} -o json" + result = run_command(command) + if result: + releases = json.loads(result) + for release in releases: + release_list.append({"namespace": namespace, "name": release["name"]}) + + confirmed = "confirmed" + click.secho("Preparing to bring down the running Warnet...", fg="yellow") + + table = Table(title="PODS TO DESTROY", show_header=True, header_style="bold red") + table.add_column("Namespace", style="red") + table.add_column("Name", style="red") + for release in release_list: + table.add_row(release["namespace"], release["name"]) + console.print(table) + click.secho("PODS WILL BE DESTROYED FOREVER IF YOU TYPE 'y'", fg="red", bg="white") + + proj_answers = inquirer.prompt( + [ + inquirer.Confirm( + confirmed, + message=click.style( + "Do you want to bring down the running Warnet?", fg="yellow", bold=False + ), + default=False, + ), + ] + ) + if not proj_answers: + click.secho("Operation cancelled by user.", fg="yellow") + sys.exit(0) + if proj_answers[confirmed]: + click.secho("Bringing down the warnet...", fg="yellow") + else: + click.secho("Operation cancelled by user", fg="yellow") + sys.exit(0) + + with ThreadPoolExecutor(max_workers=10) as executor: + futures = [] + + # Uninstall Helm releases + for release in release_list: + futures.append( + executor.submit(uninstall_release, release["namespace"], release["name"]) + ) + + # Wait for all tasks to complete and print results + for future in as_completed(futures): + console.print(f"[yellow]{future.result()}[/yellow]") + + with ThreadPoolExecutor(max_workers=10) as executor: + futures = [] + + # Delete remaining pods + pods = get_pods() + for pod in pods: + futures.append(executor.submit(delete_pod, pod.metadata.name, pod.metadata.namespace)) + + # Wait for all tasks to complete and print results + for future in as_completed(futures): + console.print(f"[yellow]{future.result()}[/yellow]") + + console.print("[bold yellow]Teardown process initiated for all components.[/bold yellow]") + console.print("[bold yellow]Note: Some processes may continue in the background.[/bold yellow]") + console.print("[bold green]Warnet teardown process completed.[/bold green]") + + +def get_active_network(namespace): + """Get the name of the active network (Helm release) in the given namespace""" + cmd = f"helm list --namespace {namespace} --output json" + result = run_command(cmd) + if result: + import json + + releases = json.loads(result) + if releases: + # Assuming the first release is the active network + return releases[0]["name"] + return None + + +@click.command(context_settings={"ignore_unknown_options": True}) +@click.argument("scenario_file", type=click.Path(exists=True, file_okay=True, dir_okay=False)) +@click.option( + "--debug", + is_flag=True, + default=False, + help="Stream scenario output and delete container when stopped", +) +@click.option( + "--source_dir", type=click.Path(exists=True, file_okay=False, dir_okay=True), required=False +) +@click.argument("additional_args", nargs=-1, type=click.UNPROCESSED) +@click.option("--admin", is_flag=True, default=False, show_default=False) +@click.option("--namespace", default=None, show_default=True) +def run( + scenario_file: str, + debug: bool, + source_dir, + additional_args: tuple[str], + admin: bool, + namespace: Optional[str], +): + """ + Run a scenario from a file. + Pass `-- --help` to get individual scenario help + """ + return _run(scenario_file, debug, source_dir, additional_args, admin, namespace) + + +def _run( + scenario_file: str, + debug: bool, + source_dir, + additional_args: tuple[str], + admin: bool, + namespace: Optional[str], +) -> str: + namespace = get_default_namespace_or(namespace) + + scenario_path = Path(scenario_file).resolve() + scenario_dir = scenario_path.parent if not source_dir else Path(source_dir).resolve() + scenario_name = scenario_path.stem + + if additional_args and ("--help" in additional_args or "-h" in additional_args): + return subprocess.run([sys.executable, scenario_path, "--help"]) + + name = f"commander-{scenario_name.replace('_', '')}-{int(time.time())}" + + # Create in-memory buffer to store python archive instead of writing to disk + archive_buffer = io.BytesIO() + + # No need to copy the entire scenarios/ directory into the archive + def filter(path): + if any(needle in str(path) for needle in [".pyc", ".csv", ".DS_Store"]): + return False + if any( + needle in str(path) + for needle in [ + "__init__.py", + "commander.py", + "test_framework", + "ln_framework", + scenario_path.name, + ] + ): + print(f"Including: {path}") + return True + return False + + # In case the scenario file is not in the root of the archive directory, + # we need to specify its relative path as a submodule + # First get the path of the file relative to the source directory + relative_path = scenario_path.relative_to(scenario_dir) + # Remove the '.py' extension + relative_name = relative_path.with_suffix("") + # Replace path separators with dots and pray the user included __init__.py + module_name = ".".join(relative_name.parts) + # Compile python archive + zipapp.create_archive( + source=scenario_dir, + target=archive_buffer, + main=f"{module_name}:main", + compressed=True, + filter=filter, + ) + + # Encode the binary data as Base64 + archive_buffer.seek(0) + archive_data = archive_buffer.read() + + # Start the commander pod with python and init containers + try: + # Construct Helm command + helm_command = [ + "helm", + "upgrade", + "--install", + "--namespace", + namespace, + "--set", + f"fullnameOverride={name}", + ] + + # Add additional arguments + if admin: + helm_command.extend(["--set", "admin=true"]) + if additional_args: + helm_command.extend(["--set", f"args={' '.join(additional_args)}"]) + + helm_command.extend([name, COMMANDER_CHART]) + + # Execute Helm command + result = subprocess.run(helm_command, check=True, capture_output=True, text=True) + + if result.returncode == 0: + print(f"Successfully deployed scenario commander: {scenario_name}") + print(f"Commander pod name: {name}") + else: + print(f"Failed to deploy scenario commander: {scenario_name}") + print(f"Error: {result.stderr}") + + except subprocess.CalledProcessError as e: + print(f"Failed to deploy scenario commander: {scenario_name}") + print(f"Error: {e.stderr}") + return None + except FileNotFoundError as e: + click.secho(e) + click.secho("Please install Helm, or run `warnet setup`.", fg="red") + return None + + # upload scenario files and network data to the init container + wait_for_init(name, namespace=namespace) + if write_file_to_container( + name, "init", "/shared/archive.pyz", archive_data, namespace=namespace + ): + print(f"Successfully uploaded scenario data to commander: {scenario_name}") + + if debug: + print("Waiting for commander pod to start...") + wait_for_pod(name, namespace=namespace) + _logs(pod_name=name, follow=True, namespace=namespace) + print("Deleting pod...") + delete_pod(name, namespace=namespace) + + return name + + +@click.command() +@click.argument("pod_name", type=str, default="") +@click.option("--follow", "-f", is_flag=True, default=False, help="Follow logs") +@click.option("--namespace", type=str, default="default", show_default=True) +def logs(pod_name: str, follow: bool, namespace: str): + """Show the logs of a pod""" + return _logs(pod_name, follow, namespace) + + +def _logs(pod_name: str, follow: bool, namespace: Optional[str] = None): + namespace = get_default_namespace_or(namespace) + + def format_pods(pods: list[V1Pod]) -> list[str]: + sorted_pods = sorted(pods, key=lambda pod: pod.metadata.creation_timestamp, reverse=True) + return [f"{pod.metadata.name}: {pod.metadata.namespace}" for pod in sorted_pods] + + if pod_name == "": + try: + pod_list = [] + formatted_commanders = format_pods(get_mission(COMMANDER_MISSION)) + formatted_tanks = format_pods(get_mission(TANK_MISSION)) + pod_list.extend(formatted_commanders) + pod_list.extend(formatted_tanks) + + except Exception as e: + print(f"Could not fetch any pods in namespace ({namespace}): {e}") + return + + if not pod_list: + print(f"Could not fetch any pods in namespace ({namespace})") + return + + q = [ + inquirer.List( + name="pod", + message="Please choose a pod", + choices=pod_list, + ) + ] + selected = inquirer.prompt(q, theme=GreenPassion()) + if selected: + pod_name, namespace = selected["pod"].split(": ") + else: + return # cancelled by user + + try: + pod = get_pod(pod_name, namespace=namespace) + eligible_container_names = [BITCOINCORE_CONTAINER, COMMANDER_CONTAINER] + available_container_names = [container.name for container in pod.spec.containers] + container_name = next( + ( + container_name + for container_name in available_container_names + if container_name in eligible_container_names + ), + None, + ) + if not container_name: + print("Could not determine primary container.") + return + except Exception as e: + print(f"Error getting pods. Could not determine primary container: {e}") + return + + try: + stream = pod_log( + pod_name, container_name=container_name, namespace=namespace, follow=follow + ) + for line in stream: + click.echo(line.decode("utf-8").rstrip()) + except Exception as e: + print(e) + except KeyboardInterrupt: + print("Interrupted streaming log!") + + +@click.command() +@click.argument("tank_name", required=False) +@click.option("--all", "-a", "snapshot_all", is_flag=True, help="Snapshot all running tanks") +@click.option( + "--output", + "-o", + type=click.Path(), + default="./warnet-snapshots", + help="Output directory for snapshots", +) +@click.option( + "--filter", + "-f", + type=str, + help="Comma-separated list of directories and/or files to include in the snapshot", +) +def snapshot(tank_name, snapshot_all, output, filter): + """Create a snapshot of a tank's Bitcoin data or snapshot all tanks""" + tanks = get_mission("tank") + + if not tanks: + console.print("[bold red]No active tanks found.[/bold red]") + return + + # Create the output directory if it doesn't exist + os.makedirs(output, exist_ok=True) + + filter_list = [f.strip() for f in filter.split(",")] if filter else None + if snapshot_all: + snapshot_all_tanks(tanks, output, filter_list) + elif tank_name: + snapshot_single_tank(tank_name, tanks, output, filter_list) + else: + select_and_snapshot_tank(tanks, output, filter_list) + + +def find_tank_by_name(tanks, tank_name): + for tank in tanks: + if tank.metadata.name == tank_name: + return tank + return None + + +def snapshot_all_tanks(tanks, output_dir, filter_list): + with console.status("[bold yellow]Snapshotting all tanks...[/bold yellow]"): + for tank in tanks: + tank_name = tank.metadata.name + chain = tank.metadata.labels["chain"] + snapshot_tank(tank_name, chain, output_dir, filter_list) + console.print("[bold green]All tank snapshots completed.[/bold green]") + + +def snapshot_single_tank(tank_name, tanks, output_dir, filter_list): + tank = find_tank_by_name(tanks, tank_name) + if tank: + chain = tank.metadata.labels["chain"] + snapshot_tank(tank_name, chain, output_dir, filter_list) + else: + console.print(f"[bold red]No active tank found with name: {tank_name}[/bold red]") + + +def select_and_snapshot_tank(tanks, output_dir, filter_list): + table = Table(title="Active Tanks", show_header=True, header_style="bold magenta") + table.add_column("Number", style="cyan", justify="right") + table.add_column("Tank Name", style="green") + + for idx, tank in enumerate(tanks, 1): + table.add_row(str(idx), tank.metadata.name) + + console.print(table) + + choices = [str(i) for i in range(1, len(tanks) + 1)] + ["q"] + choice = Prompt.ask( + "[bold yellow]Enter the number of the tank to snapshot, or 'q' to quit[/bold yellow]", + choices=choices, + show_choices=False, + ) + + if choice == "q": + console.print("[bold blue]Operation cancelled.[/bold blue]") + return + + selected_tank = tanks[int(choice) - 1] + tank_name = selected_tank.metadata.name + chain = selected_tank.metadata.labels["chain"] + snapshot_tank(tank_name, chain, output_dir, filter_list) + + +def snapshot_tank(tank_name, chain, output_dir, filter_list): + try: + output_path = Path(output_dir).resolve() + snapshot_bitcoin_datadir(tank_name, chain, str(output_path), filter_list) + console.print( + f"[bold green]Successfully created snapshot for tank: {tank_name}[/bold green]" + ) + except Exception as e: + console.print( + f"[bold red]Failed to create snapshot for tank {tank_name}: {str(e)}[/bold red]" + ) diff --git a/src/warnet/dashboard.py b/src/warnet/dashboard.py new file mode 100644 index 000000000..6eb693911 --- /dev/null +++ b/src/warnet/dashboard.py @@ -0,0 +1,25 @@ +import sys + +import click + +from .k8s import get_ingress_ip_or_host, wait_for_ingress_endpoint + + +@click.command() +def dashboard(): + """Open the Warnet dashboard in default browser""" + import webbrowser + + timeout = 300 + click.echo(f"Waiting {timeout} seconds for ingress endpoint ...") + try: + wait_for_ingress_endpoint(timeout=timeout) + except Exception as e: + click.echo(e) + sys.exit(1) + ip = get_ingress_ip_or_host() + + url = f"http://{ip}" + + webbrowser.open(url) + click.echo(f"Warnet dashboard opened in default browser. URL: {url}") diff --git a/src/warnet/deploy.py b/src/warnet/deploy.py new file mode 100644 index 000000000..6819fc306 --- /dev/null +++ b/src/warnet/deploy.py @@ -0,0 +1,550 @@ +import json +import subprocess +import sys +import tempfile +from multiprocessing import Process +from pathlib import Path +from typing import Optional + +import click +import yaml + +from .constants import ( + BITCOIN_CHART_LOCATION, + CADDY_CHART, + DEFAULTS_FILE, + DEFAULTS_NAMESPACE_FILE, + FORK_OBSERVER_CHART, + FORK_OBSERVER_RPC_PASSWORD, + FORK_OBSERVER_RPC_USER, + HELM_COMMAND, + INGRESS_HELM_COMMANDS, + LOGGING_CRD_COMMANDS, + LOGGING_HELM_COMMANDS, + LOGGING_NAMESPACE, + NAMESPACES_CHART_LOCATION, + NAMESPACES_FILE, + NETWORK_FILE, + PLUGIN_ANNEX, + SCENARIOS_DIR, + WARGAMES_NAMESPACE_PREFIX, + AnnexMember, + HookValue, + WarnetContent, +) +from .control import _logs, _run +from .k8s import ( + get_default_namespace, + get_default_namespace_or, + get_mission, + get_namespaces_by_type, + wait_for_ingress_controller, + wait_for_pod_ready, +) +from .process import run_command, stream_command + +HINT = "\nAre you trying to run a scenario? See `warnet run --help`" + + +def validate_directory(ctx, param, value): + directory = Path(value) + if not directory.is_dir(): + raise click.BadParameter(f"'{value}' is not a valid directory.{HINT}") + if not (directory / NETWORK_FILE).exists() and not (directory / NAMESPACES_FILE).exists(): + raise click.BadParameter( + f"'{value}' does not contain a valid network.yaml or namespaces.yaml file.{HINT}" + ) + return directory + + +@click.command(context_settings={"ignore_unknown_options": True}) +@click.argument( + "directory", + type=click.Path(exists=True), + callback=validate_directory, +) +@click.option("--debug", is_flag=True) +@click.option("--namespace", type=str, help="Specify a namespace in which to deploy the network") +@click.option("--to-all-users", is_flag=True, help="Deploy network to all user namespaces") +@click.argument("unknown_args", nargs=-1) +def deploy(directory, debug, namespace, to_all_users, unknown_args): + """Deploy a warnet with topology loaded from """ + if unknown_args: + raise click.BadParameter(f"Unknown args: {unknown_args}{HINT}") + + _deploy(directory, debug, namespace, to_all_users) + + +def _deploy(directory, debug, namespace, to_all_users): + """Deploy a warnet with topology loaded from """ + directory = Path(directory) + + if to_all_users: + namespaces = get_namespaces_by_type(WARGAMES_NAMESPACE_PREFIX) + processes = [] + for namespace in namespaces: + p = Process(target=_deploy, args=(directory, debug, namespace.metadata.name, False)) + p.start() + processes.append(p) + for p in processes: + p.join() + return + + if (directory / NETWORK_FILE).exists(): + run_plugins(directory, HookValue.PRE_DEPLOY, namespace) + + processes = [] + # Deploy logging CRD first to avoid synchronisation issues + deploy_logging_crd(directory, debug) + + logging_process = Process(target=deploy_logging_stack, args=(directory, debug)) + logging_process.start() + processes.append(logging_process) + + run_plugins(directory, HookValue.PRE_NETWORK, namespace) + + network_process = Process(target=deploy_network, args=(directory, debug, namespace)) + network_process.start() + + ingress_process = Process(target=deploy_ingress, args=(directory, debug)) + ingress_process.start() + processes.append(ingress_process) + + caddy_process = Process(target=deploy_caddy, args=(directory, debug)) + caddy_process.start() + processes.append(caddy_process) + + # Wait for the network process to complete + network_process.join() + + run_plugins(directory, HookValue.POST_NETWORK, namespace) + + # Start the fork observer process immediately after network process completes + fork_observer_process = Process(target=deploy_fork_observer, args=(directory, debug)) + fork_observer_process.start() + processes.append(fork_observer_process) + + # Wait for all other processes to complete + for p in processes: + p.join() + + run_plugins(directory, HookValue.POST_DEPLOY, namespace) + + elif (directory / NAMESPACES_FILE).exists(): + deploy_namespaces(directory) + else: + click.echo( + "Error: Neither network.yaml nor namespaces.yaml found in the specified directory." + ) + + +def run_plugins(directory, hook_value: HookValue, namespace, annex: Optional[dict] = None): + """Run the plugin commands within a given hook value""" + + network_file_path = directory / NETWORK_FILE + + with network_file_path.open() as f: + network_file = yaml.safe_load(f) or {} + if not isinstance(network_file, dict): + raise ValueError(f"Invalid network file structure: {network_file_path}") + + processes = [] + + plugins_section = network_file.get("plugins", {}) + hook_section = plugins_section.get(hook_value.value, {}) + for plugin_name, plugin_content in hook_section.items(): + match (plugin_name, plugin_content): + case (str(), dict()): + try: + entrypoint_path = Path(plugin_content.get("entrypoint")) + except Exception as err: + raise SyntaxError("Each plugin must have an 'entrypoint'") from err + + warnet_content = { + WarnetContent.HOOK_VALUE.value: hook_value.value, + WarnetContent.NAMESPACE.value: namespace, + PLUGIN_ANNEX: annex, + } + + cmd = ( + f"{sys.executable} {network_file_path.parent / entrypoint_path / Path('plugin.py')} entrypoint " + f"'{json.dumps(plugin_content)}' '{json.dumps(warnet_content)}'" + ) + print( + f"Queuing {hook_value.value} plugin command: {plugin_name} with {plugin_content}" + ) + + process = Process(target=run_command, args=(cmd,)) + processes.append(process) + + case _: + print( + f"The following plugin command does not match known plugin command structures: {plugin_name} {plugin_content}" + ) + sys.exit(1) + + if processes: + print(f"Starting {hook_value.value} plugins") + + for process in processes: + process.start() + + for process in processes: + process.join() + + print(f"Completed {hook_value.value} plugins") + + +def check_logging_required(directory: Path): + # check if node-defaults has logging or metrics enabled + default_file_path = directory / DEFAULTS_FILE + with default_file_path.open() as f: + default_file = yaml.safe_load(f) + if default_file.get("collectLogs", False): + return True + if default_file.get("metricsExport", False): + return True + if default_file.get("lnd", {}).get("metricsExport"): + return True + + # check to see if individual nodes have logging enabled + network_file_path = directory / NETWORK_FILE + with network_file_path.open() as f: + network_file = yaml.safe_load(f) + + nodes = network_file.get("nodes") or [] + for node in nodes: + if node.get("collectLogs", False): + return True + if node.get("metricsExport", False): + return True + if node.get("lnd", {}).get("metricsExport"): + return True + + return False + + +def deploy_logging_crd(directory: Path, debug: bool) -> bool: + """ + This function exists so we can parallelise the rest of the loggin stack + installation + """ + if not check_logging_required(directory): + return False + + click.echo( + "Found collectLogs or metricsExport in network definition, Deploying logging stack CRD" + ) + + for command in LOGGING_CRD_COMMANDS: + if not stream_command(command): + print(f"Failed to run Helm command: {command}") + return False + return True + + +def deploy_logging_stack(directory: Path, debug: bool) -> bool: + if not check_logging_required(directory): + return False + + click.echo("Deploying logging stack") + + for command in LOGGING_HELM_COMMANDS: + if not stream_command(command): + print(f"Failed to run Helm command: {command}") + return False + return True + + +def deploy_caddy(directory: Path, debug: bool): + network_file_path = directory / NETWORK_FILE + with network_file_path.open() as f: + network_file = yaml.safe_load(f) + + namespace = LOGGING_NAMESPACE + # TODO: get this from the helm chart + name = "caddy" + + # Only start if configured in the network file + if not network_file.get(name, {}).get("enabled", False): + return + + # configure reverse proxy to webservers in the network + services = [] + # built-in services + if check_logging_required(directory): + services.append( + {"title": "Grafana", "path": "/grafana/", "host": "loki-grafana", "port": 80} + ) + if network_file.get("fork_observer", {}).get("enabled", False): + services.append( + { + "title": "Fork Observer", + "path": "/fork-observer/", + "host": "fork-observer", + "port": 2323, + } + ) + # add any extra services + services += network_file.get("services", {}) + + click.echo(f"Adding services to dashboard: {json.dumps(services, indent=2)}") + + cmd = ( + f"{HELM_COMMAND} {name} {CADDY_CHART} " + f"--namespace {namespace} --create-namespace " + f"--set-json services='{json.dumps(services)}'" + ) + if debug: + cmd += " --debug" + + if not stream_command(cmd): + click.echo(f"Failed to run Helm command: {cmd}") + return + + wait_for_pod_ready(name, namespace) + click.echo("\nTo access the warnet dashboard run:\n warnet dashboard") + + +def deploy_ingress(directory: Path, debug: bool): + # Deploy ingress if either logging or fork observer is enabled + network_file_path = directory / NETWORK_FILE + with network_file_path.open() as f: + network_file = yaml.safe_load(f) + # Only start if caddy is enabled in the network file + if not network_file.get("caddy", {}).get("enabled", False): + return + click.echo("Deploying ingress controller") + + for command in INGRESS_HELM_COMMANDS: + if not stream_command(command): + print(f"Failed to run Helm command: {command}") + return False + + wait_for_ingress_controller() + + return True + + +def deploy_fork_observer(directory: Path, debug: bool) -> bool: + network_file_path = directory / NETWORK_FILE + with network_file_path.open() as f: + network_file = yaml.safe_load(f) + + # Only start if configured in the network file + if not network_file.get("fork_observer", {}).get("enabled", False): + return False + + default_namespace = get_default_namespace() + namespace = LOGGING_NAMESPACE + cmd = f"{HELM_COMMAND} 'fork-observer' {FORK_OBSERVER_CHART} --namespace {namespace} --create-namespace" + if debug: + cmd += " --debug" + + temp_override_file_path = "" + override_string = "" + + # Add an entry for each node in the graph + for i, tank in enumerate(get_mission("tank")): + node_name = tank.metadata.name + for container in tank.spec.containers: + if container.name == "bitcoincore": + for port in container.ports: + if port.name == "rpc": + rpcport = port.container_port + if port.name == "p2p": + p2pport = port.container_port + node_config = f""" +[[networks.nodes]] +id = {i} +name = "{node_name}" +description = "{node_name}.{default_namespace}.svc:{int(p2pport)}" +rpc_host = "{node_name}.{default_namespace}.svc" +rpc_port = {int(rpcport)} +rpc_user = "{FORK_OBSERVER_RPC_USER}" +rpc_password = "{FORK_OBSERVER_RPC_PASSWORD}" +""" + + override_string += node_config + + # Create yaml string using multi-line string format + override_string = override_string.strip() + v = {"config": override_string} + v["configQueryinterval"] = network_file.get("fork_observer", {}).get("configQueryinterval", 20) + yaml_string = yaml.dump(v, default_style="|", default_flow_style=False) + + # Dump to yaml tempfile + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as temp_file: + temp_file.write(yaml_string) + temp_override_file_path = Path(temp_file.name) + + cmd = f"{cmd} -f {temp_override_file_path}" + + if not stream_command(cmd): + click.echo(f"Failed to run Helm command: {cmd}") + return False + return True + + +def deploy_network(directory: Path, debug: bool = False, namespace: Optional[str] = None): + network_file_path = directory / NETWORK_FILE + namespace = get_default_namespace_or(namespace) + + with network_file_path.open() as f: + network_file = yaml.safe_load(f) + + needs_ln_init = False + supported_ln_projects = ["lnd", "cln"] + for node in network_file["nodes"]: + ln_config = node.get("ln", {}) + for key in supported_ln_projects: + if ln_config.get(key, False) and key in node and "channels" in node[key]: + needs_ln_init = True + break + if needs_ln_init: + break + + default_file_path = directory / DEFAULTS_FILE + with default_file_path.open() as f: + default_file = yaml.safe_load(f) + if any(default_file.get("ln", {}).get(key, False) for key in supported_ln_projects): + needs_ln_init = True + + processes = [] + for node in network_file["nodes"]: + p = Process(target=deploy_single_node, args=(node, directory, debug, namespace)) + p.start() + processes.append(p) + + for p in processes: + p.join() + + if needs_ln_init: + name = _run( + scenario_file=SCENARIOS_DIR / "ln_init.py", + debug=False, + source_dir=SCENARIOS_DIR, + additional_args=("--timeout-factor=0",), + admin=True, + namespace=namespace, + ) + wait_for_pod_ready(name, namespace=namespace) + _logs(pod_name=name, follow=True, namespace=namespace) + + +def deploy_single_node(node, directory: Path, debug: bool, namespace: str): + defaults_file_path = directory / DEFAULTS_FILE + click.echo(f"Deploying node: {node.get('name')}") + temp_override_file_path = "" + try: + node_name = node.get("name") + node_config_override = {k: v for k, v in node.items() if k != "name"} + + defaults_file_path = directory / DEFAULTS_FILE + cmd = f"{HELM_COMMAND} {node_name} {BITCOIN_CHART_LOCATION} --namespace {namespace} -f {defaults_file_path}" + if debug: + cmd += " --debug" + + if node_config_override: + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as temp_file: + yaml.dump(node_config_override, temp_file) + temp_override_file_path = Path(temp_file.name) + cmd = f"{cmd} -f {temp_override_file_path}" + + run_plugins( + directory, HookValue.PRE_NODE, namespace, annex={AnnexMember.NODE_NAME.value: node_name} + ) + + if not stream_command(cmd): + click.echo(f"Failed to run Helm command: {cmd}") + return + + run_plugins( + directory, + HookValue.POST_NODE, + namespace, + annex={AnnexMember.NODE_NAME.value: node_name}, + ) + + except Exception as e: + click.echo(f"Error: {e}") + return + finally: + if temp_override_file_path: + Path(temp_override_file_path).unlink() + + +def deploy_namespaces(directory: Path): + namespaces_file_path = directory / NAMESPACES_FILE + defaults_file_path = directory / DEFAULTS_NAMESPACE_FILE + + with namespaces_file_path.open() as f: + namespaces_file = yaml.safe_load(f) + + names = [n.get("name") for n in namespaces_file["namespaces"]] + for n in names: + if not n.startswith(WARGAMES_NAMESPACE_PREFIX): + click.secho( + f"Failed to create namespace: {n}. Namespaces must start with a '{WARGAMES_NAMESPACE_PREFIX}' prefix.", + fg="red", + ) + return + + processes = [] + for namespace in namespaces_file["namespaces"]: + p = Process(target=deploy_single_namespace, args=(namespace, defaults_file_path)) + p.start() + processes.append(p) + + for p in processes: + p.join() + + +def deploy_single_namespace(namespace, defaults_file_path: Path): + click.echo(f"Deploying namespace: {namespace.get('name')}") + temp_override_file_path = "" + try: + namespace_name = namespace.get("name") + namespace_config_override = {k: v for k, v in namespace.items() if k != "name"} + + cmd = f"{HELM_COMMAND} {namespace_name} {NAMESPACES_CHART_LOCATION} -f {defaults_file_path}" + + if namespace_config_override: + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as temp_file: + yaml.dump(namespace_config_override, temp_file) + temp_override_file_path = Path(temp_file.name) + cmd = f"{cmd} -f {temp_override_file_path}" + + if not stream_command(cmd): + click.echo(f"Failed to run Helm command: {cmd}") + return + except Exception as e: + click.echo(f"Error: {e}") + return + finally: + if temp_override_file_path: + Path(temp_override_file_path).unlink() + + +def is_windows(): + return sys.platform.startswith("win") + + +def run_detached_process(command): + if is_windows(): + # For Windows, use CREATE_NEW_PROCESS_GROUP and DETACHED_PROCESS + subprocess.Popen( + command, + shell=True, + stdin=None, + stdout=None, + stderr=None, + close_fds=True, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS, + ) + else: + # For Unix-like systems, use nohup and redirect output + command = f"nohup {command} > /dev/null 2>&1 &" + subprocess.Popen(command, shell=True, stdin=None, stdout=None, stderr=None, close_fds=True) + + print(f"Started detached process: {command}") diff --git a/src/warnet/graph.py b/src/warnet/graph.py new file mode 100644 index 000000000..d4ac77a59 --- /dev/null +++ b/src/warnet/graph.py @@ -0,0 +1,386 @@ +import json +import os +import random +import sys +from pathlib import Path + +import click +import inquirer +import yaml +from rich import print +from rich.console import Console +from rich.table import Table + +from resources.scenarios.ln_framework.ln import ( + CHANNEL_OPEN_START_HEIGHT, + CHANNEL_OPENS_PER_BLOCK, + Policy, +) + +from .constants import ( + DEFAULT_IMAGE_REPO, + DEFAULT_TAG, + FORK_OBSERVER_RPCAUTH, + SUPPORTED_TAGS, +) + + +@click.group(name="graph", hidden=True) +def graph(): + """Create and validate network graphs""" + + +def custom_graph( + tanks: list, + datadir: Path, + fork_observer: bool, + fork_obs_query_interval: int, + caddy: bool, + logging: bool, +): + try: + datadir.mkdir(parents=False, exist_ok=False) + except FileExistsError as e: + print(e) + print("Exiting network builder without overwriting") + sys.exit(1) + + # Generate network.yaml + nodes = [] + connections = set() + total_count = sum(int(entry["count"]) for entry in tanks) + index = 0 + + for entry in tanks: + for _ in range(int(entry["count"])): + if ":" in entry["version"] and "/" in entry["version"]: + repo, tag = entry["version"].split(":") + image = {"repository": repo, "tag": tag} + else: + image = {"tag": entry["version"]} + node = {"name": f"tank-{index:04d}", "addnode": [], "image": image} + + # Connect to other nodes + available_nodes = list(range(total_count)) + # Do not connect to self + available_nodes.remove(index) + for count in range(min(int(entry["connections"]), len(available_nodes))): + # Add neighbor connection first to create minimal round-robin + if count == 0: + next_node = (index + 1) % total_count + node["addnode"].append(f"tank-{next_node:04d}") + connections.add((index, next_node)) + if next_node in available_nodes: + available_nodes.remove(next_node) + else: + random_node = random.choice(available_nodes) + # Avoid circular loops of A -> B -> A + if (random_node, index) not in connections: + node["addnode"].append(f"tank-{random_node:04d}") + connections.add((index, random_node)) + available_nodes.remove(random_node) + + nodes.append(node) + index += 1 + + network_yaml_data = {"nodes": nodes} + network_yaml_data["fork_observer"] = { + "enabled": fork_observer, + "configQueryInterval": fork_obs_query_interval, + } + network_yaml_data["caddy"] = { + "enabled": caddy, + } + + with open(os.path.join(datadir, "network.yaml"), "w") as f: + yaml.dump(network_yaml_data, f, default_flow_style=False) + + # Generate node-defaults.yaml + defaults_yaml_content = { + "chain": "regtest", + "image": { + "repository": DEFAULT_IMAGE_REPO, + "pullPolicy": "IfNotPresent", + }, + "defaultConfig": f"rpcauth={FORK_OBSERVER_RPCAUTH}\n" + + "rpcwhitelist=forkobserver:getchaintips,getblockheader,getblockhash,getblock,getnetworkinfo\n" + + "rpcwhitelistdefault=0\n" + + "debug=rpc\n", + } + + # Configure logging + defaults_yaml_content["collectLogs"] = logging + defaults_yaml_content["metricsExport"] = logging + + with open(os.path.join(datadir, "node-defaults.yaml"), "w") as f: + yaml.dump(defaults_yaml_content, f, default_flow_style=False, sort_keys=False) + + click.echo( + f"Project '{datadir}' has been created with 'network.yaml' and 'node-defaults.yaml'." + ) + + +def inquirer_create_network(project_path: Path): + network_name_prompt = inquirer.prompt( + [ + inquirer.Text( + "network_name", + message=click.style("Enter your network name", fg="blue", bold=True), + validate=lambda _, x: len(x) > 0, + ) + ] + ) + if not network_name_prompt: + click.secho("Setup cancelled by user.", fg="yellow") + return False + + tanks = [] + while True: + table = Table(title="Current Network Population", show_header=True, header_style="magenta") + table.add_column("Version", style="cyan") + table.add_column("Count", style="green") + table.add_column("Connections", style="green") + + for entry in tanks: + table.add_row(entry["version"], entry["count"], entry["connections"]) + + Console().print(table) + + add_more_prompt = inquirer.prompt( + [ + inquirer.List( + "add_more", + message=click.style("How many nodes to add? (0 = done)", fg="blue", bold=True), + choices=["0", "4", "8", "12", "20", "50", "other"], + default="12", + ) + ] + ) + if not add_more_prompt: + click.secho("Setup cancelled by user.", fg="yellow") + return False + if add_more_prompt["add_more"].startswith("0"): + break + + if add_more_prompt["add_more"] == "other": + how_many_prompt = inquirer.prompt( + [ + inquirer.Text( + "how_many", + message=click.style("Enter the number of nodes", fg="blue", bold=True), + validate=lambda _, x: int(x) > 0, + ) + ] + ) + if not how_many_prompt: + click.secho("Setup cancelled by user.", fg="yellow") + return False + how_many = how_many_prompt["how_many"] + else: + how_many = add_more_prompt["add_more"] + + tank_details_prompt = inquirer.prompt( + [ + inquirer.List( + "version", + message=click.style( + "Which version would you like to add to network?", fg="blue", bold=True + ), + choices=["other"] + SUPPORTED_TAGS, + default=DEFAULT_TAG, + ), + inquirer.List( + "connections", + message=click.style( + "How many connections would you like each of these nodes to have?", + fg="blue", + bold=True, + ), + choices=["0", "1", "2", "8", "12", "other"], + default="8", + ), + ] + ) + if not tank_details_prompt: + click.secho("Setup cancelled by user.", fg="yellow") + return False + break + if tank_details_prompt["version"] == "other": + custom_version_prompt = inquirer.prompt( + [ + inquirer.Text( + "version", + message=click.style( + "Provide dockerhub repository/image:tag", fg="blue", bold=True + ), + validate=lambda _, x: "/" in x and ":" in x, + ) + ] + ) + if not custom_version_prompt: + click.secho("Setup cancelled by user.", fg="yellow") + return False + tank_details_prompt["version"] = custom_version_prompt["version"] + + if tank_details_prompt["connections"] == "other": + how_many_conn_prompt = inquirer.prompt( + [ + inquirer.Text( + "how_many_conn", + message=click.style( + "Enter the number of connections", fg="blue", bold=True + ), + validate=lambda _, x: int(x) > 0, + ) + ] + ) + if not how_many_conn_prompt: + click.secho("Setup cancelled by user.", fg="yellow") + return False + how_many_conn = how_many_conn_prompt["how_many_conn"] + else: + how_many_conn = tank_details_prompt["connections"] + + tanks.append( + { + "version": tank_details_prompt["version"], + "count": how_many, + "connections": how_many_conn, + } + ) + + fork_observer = click.prompt( + click.style( + "\nWould you like to enable fork-observer on the network?", fg="blue", bold=True + ), + type=bool, + default=True, + ) + fork_observer_query_interval = 20 + if fork_observer: + fork_observer_query_interval = click.prompt( + click.style( + "\nHow often would you like fork-observer to query node status (seconds)?", + fg="blue", + bold=True, + ), + type=int, + default=20, + ) + + logging = click.prompt( + click.style( + "\nWould you like to enable grafana logging on the network?", fg="blue", bold=True + ), + type=bool, + default=False, + ) + caddy = fork_observer | logging + custom_network_path = project_path / "networks" / network_name_prompt["network_name"] + click.secho("\nGenerating custom network...", fg="yellow", bold=True) + custom_graph( + tanks, + custom_network_path, + fork_observer, + fork_observer_query_interval, + caddy, + logging, + ) + return custom_network_path + + +@click.command() +def create(): + """Create a new warnet network""" + try: + project_path = Path(os.getcwd()) + # Check if the project has a networks directory + if not (project_path / "networks").exists(): + click.secho( + "The current directory does not have a 'networks' directory. Please run 'warnet init' or 'warnet new' first.", + fg="red", + bold=True, + ) + return False + custom_network_path = inquirer_create_network(project_path) + click.secho("\nNew network created successfully!", fg="green", bold=True) + click.echo("\nRun the following command to deploy this network:") + click.echo(f"warnet deploy {custom_network_path}") + except Exception as e: + click.echo(f"{e}\n\n") + click.secho(f"An error occurred while creating a new network:\n\n{e}\n\n", fg="red") + click.secho( + "Please report the above context to https://github.com/bitcoin-dev-project/warnet/issues", + fg="yellow", + ) + return False + + +@click.command() +@click.argument("graph_file_path", type=click.Path(exists=True, file_okay=True, dir_okay=False)) +@click.argument("output_path", type=click.Path(exists=False, file_okay=False, dir_okay=True)) +def import_network(graph_file_path: str, output_path: str): + """Create a network from an imported lightning network graph JSON""" + print(_import_network(graph_file_path, output_path)) + + +def _import_network(graph_file_path, output_path): + output_path = Path(output_path) + graph_file_path = Path(graph_file_path).resolve() + with open(graph_file_path) as graph_file: + graph = json.loads(graph_file.read()) + + tanks = {} + pk_to_tank = {} + tank_to_pk = {} + index = 0 + for node in graph["nodes"]: + tank = f"tank-{index:04d}" + pk_to_tank[node["pub_key"]] = tank + tank_to_pk[tank] = node["pub_key"] + tanks[tank] = {"name": tank, "ln": {"lnd": True}, "lnd": {"channels": []}} + index += 1 + print(f"Imported {index} nodes") + + sorted_edges = sorted(graph["edges"], key=lambda x: int(x["channel_id"])) + + # Start including channel open txs at this block height + block = CHANNEL_OPEN_START_HEIGHT + # Coinbase occupies the 0 position! + index = 1 + count = 0 + for edge in sorted_edges: + source = pk_to_tank[edge["node1_pub"]] + channel = { + "id": {"block": block, "index": index}, + "target": pk_to_tank[edge["node2_pub"]] + "-ln", + "capacity": int(edge["capacity"]), + "push_amt": int(edge["capacity"]) // 2, + "source_policy": Policy.from_lnd_describegraph(edge["node1_policy"]).to_dict(), + "target_policy": Policy.from_lnd_describegraph(edge["node2_policy"]).to_dict(), + } + tanks[source]["lnd"]["channels"].append(channel) + index += 1 + if index > CHANNEL_OPENS_PER_BLOCK: + index = 1 + block += 1 + count += 1 + + print(f"Imported {count} channels") + + network = {"nodes": []} + prev_node_name = list(tanks.keys())[-1] + for name, obj in tanks.items(): + obj["name"] = name + obj["addnode"] = [prev_node_name] + prev_node_name = name + network["nodes"].append(obj) + + output_path.mkdir(parents=True, exist_ok=True) + # This file must exist and must contain at least one line of valid yaml + with open(output_path / "node-defaults.yaml", "w") as f: + f.write(f"imported_from: {graph_file_path}\n") + # Here's the good stuff + with open(output_path / "network.yaml", "w") as f: + f.write(yaml.dump(network, sort_keys=False)) + return f"Network created in {output_path.resolve()}" diff --git a/src/warnet/graph_schema.json b/src/warnet/graph_schema.json deleted file mode 100644 index ac1f7aa9f..000000000 --- a/src/warnet/graph_schema.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "graph": { - "type": "object", - "properties": { - "node": {"type": "object"}, - "edge": {"type": "object"}, - "node_default": {"type": "object"}, - "edge_default": {"type": "object"}, - "services": { - "type": "string", - "comment": "A space-separated list of extra service containers to deploy in the network. See [docs/services.md](services.md) for complete list of available services" - } - }, - "additionalProperties": false - }, - "node": { - "type": "object", - "properties": { - "version": { - "type": "string", - "comment": "Bitcoin Core version with an available Warnet tank image on Dockerhub. May also be a GitHub repository with format user/repository:branch to build from source code"}, - "image": { - "type": "string", - "comment": "Bitcoin Core Warnet tank image on Dockerhub with the format repository/image:tag"}, - "bitcoin_config": { - "type": "string", - "default": "", - "comment": "A string of Bitcoin Core options in command-line format, e.g. '-debug=net -blocksonly'"}, - "tc_netem": { - "type": "string", - "comment": "A tc-netem command as a string beginning with 'tc qdisc add dev eth0 root netem'"}, - "exporter": { - "type": "boolean", - "default": false, - "comment": "Whether to attach a Prometheus data exporter to the tank"}, - "metrics": { - "type": "string", - "comment": "A space-separated string of RPC queries to scrape by Prometheus"}, - "collect_logs": { - "type": "boolean", - "default": false, - "comment": "Whether to collect Bitcoin Core debug logs with Promtail"}, - "build_args": { - "type": "string", - "default": "", - "comment": "A string of configure options used when building Bitcoin Core from source code, e.g. '--without-gui --disable-tests'"}, - "ln": { - "type": "string", - "comment": "Attach a lightning network node of this implementation (currently only supports 'lnd' or 'cln')"}, - "ln_image": { - "type": "string", - "comment": "Specify a lightning network node image from Dockerhub with the format repository/image:tag"}, - "ln_cb_image": { - "type": "string", - "comment": "Specify a lnd Circuit Breaker image from Dockerhub with the format repository/image:tag"}, - "ln_config": { - "type": "string", - "comment": "A string of arguments for the lightning network node in command-line format, e.g. '--protocol.wumbo-channels --bitcoin.timelockdelta=80'"} - }, - "additionalProperties": false, - "oneOf": [ - {"required": ["version"]}, - {"required": ["image"]} - ], - "required": [] - }, - "edge": { - "type": "object", - "properties": { - "channel_open": { - "type": "string", - "comment": "Indicate that this edge is a lightning channel with these arguments passed to lnd openchannel"}, - "source_policy": { - "type": "string", - "comment": "Update the channel originator policy by passing these arguments passed to lnd updatechanpolicy"}, - "target_policy": { - "type": "string", - "comment": "Update the channel partner policy by passing these arguments passed to lnd updatechanpolicy"} - }, - "additionalProperties": false, - "required": [] - } -} diff --git a/src/warnet/image.py b/src/warnet/image.py new file mode 100644 index 000000000..a13fe8e6e --- /dev/null +++ b/src/warnet/image.py @@ -0,0 +1,37 @@ +import sys + +import click + +from .image_build import build_image + + +@click.group(name="image") +def image(): + """Build a custom Warnet Bitcoin Core image""" + + +@image.command() +@click.option("--repo", required=True, type=str) +@click.option("--commit-sha", required=True, type=str) +@click.option( + "--tags", + required=True, + type=str, + help="Comma-separated list of full tags including image names", +) +@click.option("--build-args", required=False, type=str) +@click.option("--arches", required=False, type=str) +@click.option("--action", required=False, type=str, default="load") +def build(repo, commit_sha, tags, build_args, arches, action): + """Build a Bitcoin Core Docker image with specified parameters. + + \b + Usage Examples: + # Build an image for Warnet repository + warnet image build --repo bitcoin/bitcoin --commit-sha d6db87165c6dc2123a759c79ec236ea1ed90c0e3 --tags bitcoindevproject/bitcoin:v29.0-rc2 --arches amd64,arm64,armhf --action push + # Build an image for local testing + warnet image build --repo bitcoin/bitcoin --commit-sha d6db87165c6dc2123a759c79ec236ea1ed90c0e3 --tags bitcoindevproject/bitcoin:v29.0-rc2 --action load + """ + res = build_image(repo, commit_sha, tags, build_args, arches, action) + if not res: + sys.exit(1) diff --git a/src/warnet/cli/image_build.py b/src/warnet/image_build.py similarity index 72% rename from src/warnet/cli/image_build.py rename to src/warnet/image_build.py index 1cc7864f8..b71be6d34 100644 --- a/src/warnet/cli/image_build.py +++ b/src/warnet/image_build.py @@ -3,7 +3,7 @@ ARCHES = ["amd64", "arm64", "armhf"] -dockerfile_path = files("images.bitcoin").joinpath("Dockerfile") +dockerfile_path = files("resources.images.bitcoin").joinpath("Dockerfile.dev") def run_command(command): @@ -17,14 +17,13 @@ def run_command(command): def build_image( repo: str, commit_sha: str, - docker_registry: str, - tag: str, + tags: str, build_args: str, arches: str, action: str, ): if not build_args: - build_args = '"--disable-tests --without-gui --disable-bench --disable-fuzz-binary --enable-suppress-external-warnings --disable-dependency-tracking "' + build_args = '"-DBUILD_TESTS=OFF -DBUILD_GUI=OFF -DBUILD_BENCH=OFF -DBUILD_UTIL=ON -DBUILD_FUZZ_BINARY=OFF -DWITH_ZMQ=ON "' else: build_args = f'"{build_args}"' @@ -41,8 +40,7 @@ def build_image( print(f"{repo=:}") print(f"{commit_sha=:}") - print(f"{docker_registry=:}") - print(f"{tag=:}") + print(f"{tags=:}") print(f"{build_args=:}") print(f"{build_arches=:}") @@ -52,14 +50,13 @@ def build_image( use_builder_cmd = f"docker buildx use --builder {builder_name}" cleanup_builder_cmd = f"docker buildx rm {builder_name}" - if not run_command(create_builder_cmd): # noqa: SIM102 - # try to use existing - if not run_command(use_builder_cmd): - print(f"Could not create or use builder {builder_name} and create new builder") - return False + if not run_command(create_builder_cmd) and not run_command(use_builder_cmd): + print(f"Could not create or use builder {builder_name} and create new builder") + return False - image_full_name = f"{docker_registry}:{tag}" - print(f"{image_full_name=}") + tag_list = tags.split(",") + tag_args = " ".join([f"--tag {tag.strip()}" for tag in tag_list]) + print(f"{tag_args=}") platforms = ",".join([f"linux/{arch}" for arch in build_arches]) @@ -69,7 +66,7 @@ def build_image( f" --build-arg REPO={repo}" f" --build-arg COMMIT_SHA={commit_sha}" f" --build-arg BUILD_ARGS={build_args}" - f" --tag {image_full_name}" + f" {tag_args}" f" --file {dockerfile_path}" f" {dockerfile_path.parent}" f" --{action}" @@ -82,7 +79,6 @@ def build_image( except Exception as e: print(f"Error:\n{e}") finally: - # Tidy up the buildx builder if not run_command(cleanup_builder_cmd): print("Warning: Failed to remove the buildx builder.") else: diff --git a/src/warnet/k8s.py b/src/warnet/k8s.py new file mode 100644 index 000000000..9ca39feaa --- /dev/null +++ b/src/warnet/k8s.py @@ -0,0 +1,701 @@ +import json +import os +import sys +import tarfile +import tempfile +from pathlib import Path +from time import sleep, time +from typing import Optional + +import yaml +from kubernetes import client, config, watch +from kubernetes.client import CoreV1Api +from kubernetes.client.models import V1Namespace, V1Pod, V1PodList, V1TokenRequestSpec +from kubernetes.client.rest import ApiException +from kubernetes.dynamic import DynamicClient +from kubernetes.stream import stream + +from .constants import ( + CADDY_INGRESS_NAME, + DEFAULT_NAMESPACE, + INGRESS_NAMESPACE, + KUBE_INTERNAL_NAMESPACES, + KUBECONFIG, + LOGGING_NAMESPACE, +) +from .process import run_command, stream_command + + +class K8sError(Exception): + pass + + +def get_static_client() -> CoreV1Api: + config.load_kube_config(config_file=KUBECONFIG) + return client.CoreV1Api() + + +def get_dynamic_client() -> DynamicClient: + config.load_kube_config(config_file=KUBECONFIG) + return DynamicClient(client.ApiClient()) + + +def get_pods() -> list[V1Pod]: + sclient = get_static_client() + pods: list[V1Pod] = [] + namespaces = get_namespaces() + for ns in namespaces: + namespace = ns.metadata.name + try: + pod_list: V1PodList = sclient.list_namespaced_pod(namespace) + for pod in pod_list.items: + pods.append(pod) + except Exception as e: + raise e + return pods + + +def get_pod(name: str, namespace: Optional[str] = None) -> V1Pod: + namespace = get_default_namespace_or(namespace) + sclient = get_static_client() + return sclient.read_namespaced_pod(name=name, namespace=namespace) + + +def get_mission(mission: str) -> list[V1Pod]: + pods = get_pods() + crew: list[V1Pod] = [] + for pod in pods: + if "mission" in pod.metadata.labels and pod.metadata.labels["mission"] == mission: + crew.append(pod) + return crew + + +def get_pod_exit_status(pod_name, namespace: Optional[str] = None): + namespace = get_default_namespace_or(namespace) + try: + sclient = get_static_client() + pod = sclient.read_namespaced_pod(name=pod_name, namespace=namespace) + for container_status in pod.status.container_statuses: + if container_status.state.terminated: + return container_status.state.terminated.exit_code + return None + except client.ApiException as e: + print(f"Exception when calling CoreV1Api->read_namespaced_pod: {e}") + return None + + +def get_channels(namespace: Optional[str] = None) -> any: + namespace = get_default_namespace_or(namespace) + sclient = get_static_client() + config_maps = sclient.list_namespaced_config_map( + namespace=namespace, label_selector="channels=true" + ) + channels = [] + for cm in config_maps.items: + channel_jsons = json.loads(cm.data["channels"]) + for channel_json in channel_jsons: + channel_json["source"] = cm.data["source"] + channels.append(channel_json) + return channels + + +def create_kubernetes_object( + kind: str, metadata: dict[str, any], spec: dict[str, any] = None +) -> dict[str, any]: + metadata["namespace"] = get_default_namespace() + obj = { + "apiVersion": "v1", + "kind": kind, + "metadata": metadata, + } + if spec is not None: + obj["spec"] = spec + return obj + + +def set_kubectl_context(namespace: str) -> bool: + """ + Set the default kubectl context to the specified namespace. + """ + command = f"kubectl config set-context --current --namespace={namespace}" + result = stream_command(command) + if result: + print(f"Kubectl context set to namespace: {namespace}") + else: + print(f"Failed to set kubectl context to namespace: {namespace}") + return result + + +def apply_kubernetes_yaml(yaml_file: str) -> bool: + command = f"kubectl apply -f {yaml_file}" + return stream_command(command) + + +def apply_kubernetes_yaml_obj(yaml_obj: str) -> None: + with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as temp_file: + yaml.dump(yaml_obj, temp_file) + temp_file_path = temp_file.name + + try: + apply_kubernetes_yaml(temp_file_path) + finally: + Path(temp_file_path).unlink() + + +def delete_namespace(namespace: str) -> bool: + command = f"kubectl delete namespace {namespace} --ignore-not-found" + return run_command(command) + + +def delete_pod(pod_name: str, namespace: Optional[str] = None) -> bool: + namespace = get_default_namespace_or(namespace) + command = f"kubectl -n {namespace} delete pod {pod_name}" + return stream_command(command) + + +def get_default_namespace() -> str: + command = "kubectl config view --minify -o jsonpath='{..namespace}'" + try: + kubectl_namespace = run_command(command) + except Exception as e: + print(e) + if str(e).find("command not found"): + print( + "It looks like kubectl is not installed. Please install it to continue: " + "https://kubernetes.io/docs/tasks/tools/" + ) + sys.exit(1) + return kubectl_namespace if kubectl_namespace else DEFAULT_NAMESPACE + + +def get_default_namespace_or(namespace: Optional[str]) -> str: + return namespace if namespace else get_default_namespace() + + +def snapshot_bitcoin_datadir( + pod_name: str, + chain: str, + local_path: str = "./", + filters: list[str] = None, + namespace: Optional[str] = None, +) -> None: + namespace = get_default_namespace_or(namespace) + sclient = get_static_client() + + try: + sclient.read_namespaced_pod(name=pod_name, namespace=namespace) + + # Filter down to the specified list of directories and files + # This allows for creating snapshots of only the relevant data, e.g., + # we may want to snapshot the blocks but not snapshot peers.dat or the node + # wallets. + # + # TODO: never snapshot bitcoin.conf, as this is managed by the helm config + if filters: + find_command = [ + "find", + f"/root/.bitcoin/{chain}", + "(", + "-type", + "f", + "-o", + "-type", + "d", + ")", + "(", + "-name", + filters[0], + ] + for f in filters[1:]: + find_command.extend(["-o", "-name", f]) + find_command.append(")") + else: + # If no filters, get everything in the Bitcoin directory (TODO: exclude bitcoin.conf) + find_command = ["find", f"/root/.bitcoin/{chain}"] + + resp = stream( + sclient.connect_get_namespaced_pod_exec, + pod_name, + namespace, + command=find_command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + _preload_content=False, + ) + + file_list = [] + while resp.is_open(): + resp.update(timeout=1) + if resp.peek_stdout(): + file_list.extend(resp.read_stdout().strip().split("\n")) + if resp.peek_stderr(): + print(f"Error: {resp.read_stderr()}") + + resp.close() + if not file_list: + print("No matching files or directories found.") + return + tar_command = ["tar", "-czf", "/tmp/bitcoin_data.tar.gz", "-C", f"/root/.bitcoin/{chain}"] + tar_command.extend( + [os.path.relpath(f, f"/root/.bitcoin/{chain}") for f in file_list if f.strip()] + ) + resp = stream( + sclient.connect_get_namespaced_pod_exec, + pod_name, + namespace, + command=tar_command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + _preload_content=False, + ) + while resp.is_open(): + resp.update(timeout=1) + if resp.peek_stdout(): + print(f"Tar output: {resp.read_stdout()}") + if resp.peek_stderr(): + print(f"Error: {resp.read_stderr()}") + resp.close() + local_file_path = Path(local_path) / f"{pod_name}_bitcoin_data.tar.gz" + copy_command = ( + f"kubectl cp {namespace}/{pod_name}:/tmp/bitcoin_data.tar.gz {local_file_path}" + ) + if not stream_command(copy_command): + raise Exception("Failed to copy tar file from pod to local machine") + + print(f"Bitcoin data exported successfully to {local_file_path}") + cleanup_command = ["rm", "/tmp/bitcoin_data.tar.gz"] + stream( + sclient.connect_get_namespaced_pod_exec, + pod_name, + namespace, + command=cleanup_command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + ) + + print("To untar and repopulate the directory, use the following command:") + print(f"tar -xzf {local_file_path} -C /path/to/destination/.bitcoin/{chain}") + + except Exception as e: + print(f"An error occurred: {str(e)}") + + +def wait_for_pod_ready(name, namespace, timeout=300): + sclient = get_static_client() + w = watch.Watch() + for event in w.stream( + sclient.list_namespaced_pod, namespace=namespace, timeout_seconds=timeout + ): + pod = event["object"] + if pod.metadata.name == name and pod.status.phase == "Running": + conditions = pod.status.conditions or [] + ready_condition = next((c for c in conditions if c.type == "Ready"), None) + if ready_condition and ready_condition.status == "True": + w.stop() + return True + print(f"Timeout waiting for pod {name} to be ready.") + return False + + +def wait_for_init(pod_name, timeout=300, namespace: Optional[str] = None, quiet: bool = False): + namespace = get_default_namespace_or(namespace) + sclient = get_static_client() + w = watch.Watch() + for event in w.stream( + sclient.list_namespaced_pod, namespace=namespace, timeout_seconds=timeout + ): + pod = event["object"] + if pod.metadata.name == pod_name: + if not pod.status.init_container_statuses: + continue + for init_container_status in pod.status.init_container_statuses: + if init_container_status.state.running: + if not quiet: + print(f"initContainer in pod {pod_name} ({namespace}) is ready") + w.stop() + return True + if not quiet: + print(f"Timeout waiting for initContainer in {pod_name} ({namespace}) to be ready.") + return False + + +def wait_for_ingress_controller(timeout=300): + # get name of ingress controller pod + sclient = get_static_client() + pods = sclient.list_namespaced_pod(namespace=INGRESS_NAMESPACE) + for pod in pods.items: + if "ingress-nginx-controller" in pod.metadata.name: + return wait_for_pod_ready(pod.metadata.name, INGRESS_NAMESPACE, timeout) + + +def wait_for_ingress_endpoint(timeout=300): + config.load_kube_config() + networking_v1 = client.NetworkingV1Api() + start = time() + while time() - start < timeout: + try: + ingress = networking_v1.read_namespaced_ingress(CADDY_INGRESS_NAME, LOGGING_NAMESPACE) + except ApiException as e: + msg = ( + f'Failed to read ingress with name "{CADDY_INGRESS_NAME}" from namespace "{LOGGING_NAMESPACE}"\n' + + str(e).rstrip() + ) + if e.status == 404: + msg += "\n\nDid you deploy a network with caddy enabled?" + raise Exception(msg) from e + lb_ingress = ingress.status.load_balancer.ingress + if lb_ingress and (lb_ingress[0].hostname or lb_ingress[0].ip): + return True + sleep(1) + msg = ( + f"Ingress endpoint not found within {timeout} seconds.\n" + + "If you are running Minikube please run 'minikube tunnel' in a separate terminal.\n" + + "If you are running in the cloud, you may need to wait a short while while the load balancer is provisioned" + ) + raise TimeoutError(msg) + + +def get_ingress_ip_or_host(): + config.load_kube_config() + networking_v1 = client.NetworkingV1Api() + try: + ingress = networking_v1.read_namespaced_ingress(CADDY_INGRESS_NAME, LOGGING_NAMESPACE) + if ingress.status.load_balancer.ingress[0].hostname: + return ingress.status.load_balancer.ingress[0].hostname + return ingress.status.load_balancer.ingress[0].ip + except Exception as e: + print(f"Error getting ingress IP: {e}") + return None + + +def pod_log( + pod_name, container_name=None, follow=False, namespace: Optional[str] = None, tail_lines=None +): + namespace = get_default_namespace_or(namespace) + sclient = get_static_client() + + try: + return sclient.read_namespaced_pod_log( + name=pod_name, + namespace=namespace, + container=container_name, + follow=follow, + _preload_content=False, + tail_lines=tail_lines, + ) + except ApiException as e: + raise Exception(json.loads(e.body.decode("utf-8"))["message"]) from None + + +def wait_for_pod(pod_name, timeout_seconds=10, namespace: Optional[str] = None): + namespace = get_default_namespace_or(namespace) + sclient = get_static_client() + while timeout_seconds > 0: + pod = sclient.read_namespaced_pod_status(name=pod_name, namespace=namespace) + if pod.status.phase != "Pending": + return + sleep(1) + timeout_seconds -= 1 + + +def write_file_to_container( + pod_name, container_name, dst_path, data, namespace: Optional[str] = None, quiet: bool = False +): + namespace = get_default_namespace_or(namespace) + sclient = get_static_client() + exec_command = ["sh", "-c", f"cat > {dst_path}.tmp && sync"] + try: + res = stream( + sclient.connect_get_namespaced_pod_exec, + pod_name, + namespace, + command=exec_command, + container=container_name, + stdin=True, + stderr=True, + stdout=True, + tty=False, + _preload_content=False, + ) + res.write_stdin(data) + res.close() + rename_command = ["sh", "-c", f"mv {dst_path}.tmp {dst_path}"] + stream( + sclient.connect_get_namespaced_pod_exec, + pod_name, + namespace, + command=rename_command, + container=container_name, + stdin=False, + stderr=True, + stdout=True, + tty=False, + ) + if not quiet: + print(f"Successfully copied data to {pod_name}({container_name}):{dst_path}") + return True + except Exception as e: + print(f"Failed to copy data to {pod_name}({container_name}):{dst_path}:\n{e}") + + +def get_kubeconfig_value(jsonpath): + command = f"kubectl config view --minify --raw -o jsonpath={jsonpath}" + return run_command(command) + + +def get_cluster_of_current_context(kubeconfig_data: dict) -> dict: + # Get the current context name + current_context_name = kubeconfig_data.get("current-context") + + if not current_context_name: + raise K8sError("No current context found in kubeconfig.") + + # Find the context entry for the current context + context_entry = next( + ( + context + for context in kubeconfig_data.get("contexts", []) + if context["name"] == current_context_name + ), + None, + ) + + if not context_entry: + raise K8sError(f"Context '{current_context_name}' not found in kubeconfig.") + + # Get the cluster name from the context entry + cluster_name = context_entry.get("context", {}).get("cluster") + + if not cluster_name: + raise K8sError(f"Cluster not specified in context '{current_context_name}'.") + + # Find the cluster entry associated with the cluster name + cluster_entry = next( + ( + cluster + for cluster in kubeconfig_data.get("clusters", []) + if cluster["name"] == cluster_name + ), + None, + ) + + if not cluster_entry: + raise K8sError(f"Cluster '{cluster_name}' not found in kubeconfig.") + + return cluster_entry + + +def get_namespaces() -> list[V1Namespace]: + sclient = get_static_client() + try: + return [ + ns + for ns in sclient.list_namespace().items + if ns.metadata.name not in KUBE_INTERNAL_NAMESPACES + ] + + except ApiException as e: + if e.status == 403: + ns = sclient.read_namespace(name=get_default_namespace()) + return [ns] + else: + return [] + + +def get_namespaces_by_type(namespace_type: str) -> list[V1Namespace]: + """ + Get all namespaces beginning with `prefix`. Returns empty list of no namespaces with the specified prefix are found. + """ + namespaces = get_namespaces() + return [ns for ns in namespaces if ns.metadata.name.startswith(namespace_type)] + + +def get_warnet_user_service_accounts_in_namespace(namespace): + """ + Get all service accounts in a namespace that were created for human users + (not scenario commanders or other pods) + Returns an empty list if no applicable service accounts are found in the specified namespace. + """ + sclient = get_static_client() + sas = sclient.list_namespaced_service_account(namespace) + return [ + sa + for sa in sas.items + if sa.metadata.labels + and "mission" in sa.metadata.labels + and sa.metadata.labels["mission"] == "user" + ] + + +def get_token_for_service_acount(sa, duration): + sclient = get_static_client() + spec = V1TokenRequestSpec( + audiences=["https://kubernetes.default.svc"], expiration_seconds=duration + ) + resp = sclient.create_namespaced_service_account_token( + name=sa.metadata.name, namespace=sa.metadata.namespace, body=spec + ) + return resp.status.token + + +def can_delete_pods(namespace: Optional[str] = None) -> bool: + namespace = get_default_namespace_or(namespace) + + get_static_client() + auth_api = client.AuthorizationV1Api() + + # Define the SelfSubjectAccessReview request for deleting pods + access_review = client.V1SelfSubjectAccessReview( + spec=client.V1SelfSubjectAccessReviewSpec( + resource_attributes=client.V1ResourceAttributes( + namespace=namespace, + verb="delete", # Action: 'delete' + resource="pods", # Resource: 'pods' + ) + ) + ) + + try: + # Perform the SelfSubjectAccessReview check + review_response = auth_api.create_self_subject_access_review(body=access_review) + + # Check the result and return + if review_response.status.allowed: + print(f"Service account can delete pods in namespace '{namespace}'.") + return True + else: + print(f"Service account CANNOT delete pods in namespace '{namespace}'.") + return False + + except ApiException as e: + print(f"An error occurred: {e}") + return False + + +def open_kubeconfig(kubeconfig_path: str) -> dict: + try: + with open(kubeconfig_path) as file: + return yaml.safe_load(file) + except FileNotFoundError as e: + raise K8sError(f"Kubeconfig file {kubeconfig_path} not found.") from e + except yaml.YAMLError as e: + raise K8sError(f"Error parsing kubeconfig: {e}") from e + + +def write_kubeconfig(kube_config: dict, kubeconfig_path: str) -> None: + dir_name = os.path.dirname(kubeconfig_path) + try: + with tempfile.NamedTemporaryFile("w", dir=dir_name, delete=False) as temp_file: + yaml.safe_dump(kube_config, temp_file) + os.replace(temp_file.name, kubeconfig_path) + except Exception as e: + os.remove(temp_file.name) + raise K8sError(f"Error writing kubeconfig: {kubeconfig_path}") from e + + +def download( + pod_name: str, + source_path: Path, + destination_path: Path = Path("."), + namespace: Optional[str] = None, +) -> Path: + """Download the item from the `source_path` to the `destination_path`""" + + namespace = get_default_namespace_or(namespace) + + v1 = get_static_client() + + target_folder = destination_path / source_path.stem + os.makedirs(target_folder, exist_ok=True) + + command = ["tar", "cf", "-", "-C", str(source_path.parent), str(source_path.name)] + + resp = stream( + v1.connect_get_namespaced_pod_exec, + name=pod_name, + namespace=namespace, + command=command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + _preload_content=False, + ) + + tar_file = target_folder.with_suffix(".tar") + with open(tar_file, "wb") as f: + while resp.is_open(): + resp.update(timeout=1) + if resp.peek_stdout(): + f.write(resp.read_stdout().encode("utf-8")) + if resp.peek_stderr(): + print(resp.read_stderr()) + resp.close() + + with tarfile.open(tar_file, "r") as tar: + tar.extractall(path=destination_path) + + os.remove(tar_file) + + return destination_path + + +def read_file_from_container( + pod_name, + source_path: Path, + container_name: str = "", + namespace: Optional[str] = None, + quiet: bool = False, +) -> str: + """Download the file from the `source_path` to the `destination_path`""" + + namespace = get_default_namespace_or(namespace) + + v1 = get_static_client() + + command = ["cat", str(source_path)] + + resp = stream( + v1.connect_get_namespaced_pod_exec, + name=pod_name, + namespace=namespace, + container=container_name, + command=command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + _preload_content=False, + ) + + result = "" + while resp.is_open(): + resp.update(timeout=5) + if resp.peek_stdout(): + result += resp.read_stdout() + if resp.peek_stderr(): + raise Exception(resp.read_stderr()) + resp.close() + return result + + +def copyfile(pod_name, src_container, source_path, dst_name, dst_container, dst_path): + namespace = get_default_namespace() + file_data = read_file_from_container(pod_name, source_path, src_container, namespace) + if write_file_to_container( + dst_name, + dst_container, + dst_path, + file_data, + namespace=namespace, + quiet=True, + ): + print(f"Copied {source_path} to {dst_path}") + else: + print(f"Failed to copy {source_path} from {pod_name} to {dst_name}:{dst_path}") diff --git a/src/warnet/ln.py b/src/warnet/ln.py new file mode 100644 index 000000000..e01df85f8 --- /dev/null +++ b/src/warnet/ln.py @@ -0,0 +1,82 @@ +import json +from typing import Optional + +import click + +from .k8s import ( + get_default_namespace_or, + get_pod, +) +from .process import run_command + + +@click.group(name="ln") +def ln(): + """Control running lightning nodes""" + + +@ln.command(context_settings={"ignore_unknown_options": True}) +@click.argument("pod", type=str) +@click.argument("method", type=str) +@click.argument("params", type=str, nargs=-1) # this will capture all remaining arguments +@click.option("--namespace", default=None, show_default=True) +def rpc(pod: str, method: str, params: str, namespace: Optional[str]): + """ + Call lightning cli rpc on + """ + print(_rpc(pod, method, params, namespace)) + + +def _rpc(pod_name: str, method: str, params: str = "", namespace: Optional[str] = None): + namespace = get_default_namespace_or(namespace) + pod = get_pod(pod_name, namespace) + chain = pod.metadata.labels["chain"] + ln_client = "lncli" + if "cln" in pod.metadata.labels["app.kubernetes.io/name"]: + ln_client = "lightning-cli" + cmd = f"kubectl -n {namespace} exec {pod_name} -- {ln_client} --network {chain} {method} {' '.join(map(str, params))}" + return run_command(cmd) + + +@ln.command() +@click.argument("pod", type=str) +def pubkey( + pod: str, +): + """ + Get lightning node pub key from + """ + print(_pubkey(pod)) + + +def _pubkey(pod_name: str): + info = _rpc(pod_name, "getinfo") + pod = get_pod(pod_name) + pubkey_key = "identity_pubkey" + if "cln" in pod.metadata.labels["app.kubernetes.io/name"]: + pubkey_key = "id" + return json.loads(info)[pubkey_key] + + +@ln.command() +@click.argument("pod", type=str) +def host( + pod: str, +): + """ + Get lightning node host from + """ + print(_host(pod)) + + +def _host(pod_name: str): + info = _rpc(pod_name, "getinfo") + pod = get_pod(pod_name) + if "cln" in pod.metadata.labels["app.kubernetes.io/name"]: + return json.loads(info)["alias"] + else: + uris = json.loads(info)["uris"] + if uris and len(uris) >= 0: + return uris[0].split("@")[1] + else: + return "" diff --git a/src/warnet/lnchannel.py b/src/warnet/lnchannel.py deleted file mode 100644 index 2d17460fb..000000000 --- a/src/warnet/lnchannel.py +++ /dev/null @@ -1,148 +0,0 @@ -import logging - - -class LNPolicy: - def __init__( - self, - min_htlc: int, - max_htlc: int, - base_fee_msat: int, - fee_rate_milli_msat: int, - time_lock_delta: int = 0, - ) -> None: - self.min_htlc = min_htlc - self.max_htlc = max_htlc - self.base_fee_msat = base_fee_msat - self.fee_rate_milli_msat = fee_rate_milli_msat - self.time_lock_delta = time_lock_delta - - def __str__(self) -> str: - return ( - f"LNPolicy(min_htlc={self.min_htlc}, " - f"max_htlc={self.max_htlc}, " - f"base_fee={self.base_fee_msat}, " - f"fee_rate={self.fee_rate_milli_msat}, " - f"time_lock_delta={self.time_lock_delta})" - ) - - -class LNChannel: - def __init__( - self, - node1_pub: str, - node2_pub: str, - capacity_msat: int = 0, - short_chan_id: str = "", - node1_policy: LNPolicy = None, - node2_policy: LNPolicy = None, - ) -> None: - # Ensure that the node with the lower pubkey is node1 - if node1_pub > node2_pub: - node1_pub, node2_pub = node2_pub, node1_pub - node1_policy, node2_policy = node2_policy, node1_policy - self.node1_pub = node1_pub - self.node2_pub = node2_pub - self.capacity_msat = capacity_msat - self.short_chan_id = short_chan_id - self.node1_policy = node1_policy - self.node2_policy = node2_policy - self.logger = logging.getLogger("lnchan") - - def __str__(self) -> str: - return ( - f"LNChannel(short_chan_id={self.short_chan_id}, " - f"capacity_msat={self.capacity_msat}, " - f"node1_pub={self.node1_pub[:8]}..., " - f"node2_pub={self.node2_pub[:8]}..., " - f"node1_policy=({self.node1_policy.__str__()}), " - f"node2_policy=({self.node2_policy.__str__()}))" - ) - - # Only used to compare warnet channels imported from a mainnet source file - # because pubkeys are unpredictable and node 1/2 might be swapped - def flip(self) -> "LNChannel": - return LNChannel( - # Keep the old pubkeys so the constructor doesn't just flip it back - node1_pub=self.node1_pub, - node2_pub=self.node2_pub, - capacity_msat=self.capacity_msat, - short_chan_id=self.short_chan_id, - node1_policy=self.node2_policy, - node2_policy=self.node1_policy, - ) - - def policy_match(self, ch2: "LNChannel") -> bool: - assert isinstance(ch2, LNChannel) - - node1_policy_match = False - node2_policy_match = False - - if self.node1_policy is None and ch2.node1_policy is None: - node1_policy_match = True - - if self.node2_policy is None and ch2.node2_policy is None: - node2_policy_match = True - - def compare_attributes(attr1, attr2, min_value=0, attr_name=""): - if attr1 == 0 or attr2 == 0: - return True - result = max(int(attr1), min_value) == max(int(attr2), min_value) - if not result: - self.logger.debug(f"Mismatch in {attr_name}: {attr1} != {attr2}") - return result - - if self.node1_policy is not None and ch2.node1_policy is not None: - attributes_to_compare = [ - ( - self.node1_policy.time_lock_delta, - ch2.node1_policy.time_lock_delta, - 18, - "node1_time_lock_delta", - ), - (self.node1_policy.min_htlc, ch2.node1_policy.min_htlc, 1, "node1_min_htlc"), - ( - self.node1_policy.base_fee_msat, - ch2.node1_policy.base_fee_msat, - 0, - "node1_base_fee_msat", - ), - ( - self.node1_policy.fee_rate_milli_msat, - ch2.node1_policy.fee_rate_milli_msat, - 0, - "node1_fee_rate_milli_msat", - ), - ] - node1_policy_match = all(compare_attributes(*attrs) for attrs in attributes_to_compare) - - if self.node2_policy is not None and ch2.node2_policy is not None: - attributes_to_compare = [ - ( - self.node2_policy.time_lock_delta, - ch2.node2_policy.time_lock_delta, - 18, - "node2_time_lock_delta", - ), - (self.node2_policy.min_htlc, ch2.node2_policy.min_htlc, 1, "node2_min_htlc"), - ( - self.node2_policy.base_fee_msat, - ch2.node2_policy.base_fee_msat, - 0, - "node2_base_fee_msat", - ), - ( - self.node2_policy.fee_rate_milli_msat, - ch2.node2_policy.fee_rate_milli_msat, - 0, - "node2_fee_rate_milli_msat", - ), - ] - node2_policy_match = all(compare_attributes(*attrs) for attrs in attributes_to_compare) - - return node1_policy_match and node2_policy_match - - def channel_match(self, ch2: "LNChannel") -> bool: - if self.capacity_msat != ch2.capacity_msat: - self.logger.debug(f"Capacity mismatch: {self.capacity_msat} != {ch2.capacity_msat}") - return False - return self.policy_match(ch2) diff --git a/src/warnet/lnd.py b/src/warnet/lnd.py deleted file mode 100644 index 3282d8253..000000000 --- a/src/warnet/lnd.py +++ /dev/null @@ -1,191 +0,0 @@ -import io -import tarfile - -from warnet.backend.kubernetes_backend import KubernetesBackend -from warnet.services import ServiceType -from warnet.utils import exponential_backoff, generate_ipv4_addr, handle_json - -from .lnchannel import LNChannel, LNPolicy -from .lnnode import LNNode, lnd_to_cl_scid -from .status import RunningStatus - -LND_CONFIG_BASE = " ".join( - [ - "--noseedbackup", - "--norest", - "--debuglevel=debug", - "--accept-keysend", - "--bitcoin.active", - "--bitcoin.regtest", - "--bitcoin.node=bitcoind", - "--maxpendingchannels=64", - "--trickledelay=1", - ] -) - - -class LNDNode(LNNode): - def __init__(self, warnet, tank, backend: KubernetesBackend, options): - self.warnet = warnet - self.tank = tank - self.backend = backend - self.image = options["ln_image"] - self.cb = options["cb_image"] - self.ln_config = options["ln_config"] - self.ipv4 = generate_ipv4_addr(self.warnet.subnet) - self.rpc_port = 10009 - self.impl = "lnd" - - @property - def status(self) -> RunningStatus: - return super().status - - @property - def cb_status(self) -> RunningStatus: - return super().cb_status - - def get_conf(self, ln_container_name, tank_container_name) -> str: - conf = LND_CONFIG_BASE - conf += f" --bitcoind.rpcuser={self.tank.rpc_user}" - conf += f" --bitcoind.rpcpass={self.tank.rpc_password}" - conf += f" --bitcoind.rpchost={tank_container_name}:{self.tank.rpc_port}" - conf += f" --bitcoind.zmqpubrawblock=tcp://{tank_container_name}:{self.tank.zmqblockport}" - conf += f" --bitcoind.zmqpubrawtx=tcp://{tank_container_name}:{self.tank.zmqtxport}" - conf += f" --rpclisten=0.0.0.0:{self.rpc_port}" - conf += f" --alias={self.tank.index}" - conf += f" --externalhosts={ln_container_name}" - conf += f" --tlsextradomain={ln_container_name}" - conf += " " + self.ln_config - return conf - - @exponential_backoff(max_retries=20, max_delay=300) - @handle_json - def lncli(self, cmd) -> dict: - cli = "lncli" - cmd = f"{cli} --network=regtest {cmd}" - return self.backend.exec_run(self.tank.index, ServiceType.LIGHTNING, cmd) - - def getnewaddress(self): - return self.lncli("newaddress p2wkh")["address"] - - def get_pub_key(self): - res = self.lncli("getinfo") - return res["identity_pubkey"] - - def getURI(self): - res = self.lncli("getinfo") - if len(res["uris"]) < 1: - return None - return res["uris"][0] - - def get_wallet_balance(self) -> int: - res = self.lncli("walletbalance")["confirmed_balance"] - return res - - # returns the channel point in the form txid:output_index - def open_channel_to_tank(self, index: int, channel_open_data: str) -> str: - tank = self.warnet.tanks[index] - [pubkey, host] = tank.lnnode.getURI().split("@") - txid = self.lncli(f"openchannel --node_key={pubkey} --connect={host} {channel_open_data}")[ - "funding_txid" - ] - # Why doesn't LND return the output index as well? - # Do they charge by the RPC call or something?! - pending = self.lncli("pendingchannels") - for chan in pending["pending_open_channels"]: - if txid in chan["channel"]["channel_point"]: - return chan["channel"]["channel_point"] - raise Exception(f"Opened channel with txid {txid} not found in pending channels") - - def update_channel_policy(self, chan_point: str, policy: str) -> str: - ret = self.lncli(f"updatechanpolicy --chan_point={chan_point} {policy}") - if len(ret["failed_updates"]) == 0: - return ret - else: - raise Exception(ret) - - def get_graph_nodes(self) -> list[str]: - return list(n["pub_key"] for n in self.lncli("describegraph")["nodes"]) - - def get_graph_channels(self) -> list[LNChannel]: - edges = self.lncli("describegraph")["edges"] - return [self.lnchannel_from_json(edge) for edge in edges] - - @staticmethod - def lnchannel_from_json(edge: object) -> LNChannel: - node1_policy = ( - LNPolicy( - min_htlc=int(edge["node1_policy"]["min_htlc"]), - max_htlc=int(edge["node1_policy"]["max_htlc_msat"]), - base_fee_msat=int(edge["node1_policy"]["fee_base_msat"]), - fee_rate_milli_msat=int(edge["node1_policy"]["fee_rate_milli_msat"]), - time_lock_delta=int(edge["node1_policy"]["time_lock_delta"]), - ) - if edge["node1_policy"] - else None - ) - - node2_policy = ( - LNPolicy( - min_htlc=int(edge["node2_policy"]["min_htlc"]), - max_htlc=int(edge["node2_policy"]["max_htlc_msat"]), - base_fee_msat=int(edge["node2_policy"]["fee_base_msat"]), - fee_rate_milli_msat=int(edge["node2_policy"]["fee_rate_milli_msat"]), - time_lock_delta=int(edge["node2_policy"]["time_lock_delta"]), - ) - if edge["node2_policy"] - else None - ) - - return LNChannel( - node1_pub=edge["node1_pub"], - node2_pub=edge["node2_pub"], - capacity_msat=(int(edge["capacity"]) * 1000), - short_chan_id=lnd_to_cl_scid(edge["channel_id"]), - node1_policy=node1_policy, - node2_policy=node2_policy, - ) - - def get_peers(self) -> list[str]: - return list(p["pub_key"] for p in self.lncli("listpeers")["peers"]) - - def connect_to_tank(self, index): - return super().connect_to_tank(index) - - def generate_cli_command(self, command: list[str]): - network = f"--network={self.tank.warnet.bitcoin_network}" - cmd = f"{network} {' '.join(command)}" - cmd = f"lncli {cmd}" - return cmd - - def export(self, config: object, tar_file): - # Retrieve the credentials - macaroon = self.backend.get_file( - self.tank.index, - ServiceType.LIGHTNING, - "/root/.lnd/data/chain/bitcoin/regtest/admin.macaroon", - ) - cert = self.backend.get_file(self.tank.index, ServiceType.LIGHTNING, "/root/.lnd/tls.cert") - name = f"ln-{self.tank.index}" - macaroon_filename = f"{name}_admin.macaroon" - cert_filename = f"{name}_tls.cert" - host = self.backend.get_lnnode_hostname(self.tank.index) - - # Add the files to the in-memory tar archive - tarinfo1 = tarfile.TarInfo(name=macaroon_filename) - tarinfo1.size = len(macaroon) - fileobj1 = io.BytesIO(macaroon) - tar_file.addfile(tarinfo=tarinfo1, fileobj=fileobj1) - tarinfo2 = tarfile.TarInfo(name=cert_filename) - tarinfo2.size = len(cert) - fileobj2 = io.BytesIO(cert) - tar_file.addfile(tarinfo=tarinfo2, fileobj=fileobj2) - - config["nodes"].append( - { - "id": name, - "address": f"https://{host}:{self.rpc_port}", - "macaroon": f"/simln/{macaroon_filename}", - "cert": f"/simln/{cert_filename}", - } - ) diff --git a/src/warnet/lnnode.py b/src/warnet/lnnode.py deleted file mode 100644 index deda5da20..000000000 --- a/src/warnet/lnnode.py +++ /dev/null @@ -1,99 +0,0 @@ -from abc import ABC, abstractmethod - -from warnet.backend.kubernetes_backend import KubernetesBackend -from warnet.services import ServiceType -from warnet.utils import exponential_backoff, handle_json - -from .status import RunningStatus - - -class LNNode(ABC): - @abstractmethod - def __init__(self, warnet, tank, backend: KubernetesBackend, options): - pass - - @property - def status(self) -> RunningStatus: - return self.warnet.container_interface.get_status(self.tank.index, ServiceType.LIGHTNING) - - @property - def cb_status(self) -> RunningStatus: - if not self.cb: - return None - return self.warnet.container_interface.get_status( - self.tank.index, ServiceType.CIRCUITBREAKER - ) - - @abstractmethod - def get_conf(self, ln_container_name, tank_container_name) -> str: - pass - - @exponential_backoff(max_retries=20, max_delay=300) - @handle_json - @abstractmethod - def lncli(self, cmd) -> dict: - pass - - @abstractmethod - def getnewaddress(self): - pass - - @abstractmethod - def get_pub_key(self): - pass - - @abstractmethod - def getURI(self): - pass - - @abstractmethod - def get_wallet_balance(self) -> int: - pass - - @abstractmethod - def open_channel_to_tank(self, index: int, channel_open_data: str) -> str: - """Return the channel point in the form txid:output_index""" - pass - - @abstractmethod - def update_channel_policy(self, chan_point: str, policy: str) -> str: - pass - - @abstractmethod - def get_graph_nodes(self) -> list[str]: - pass - - @abstractmethod - def get_graph_channels(self) -> list[dict]: - pass - - @abstractmethod - def get_peers(self) -> list[str]: - pass - - def connect_to_tank(self, index): - tank = self.warnet.tanks[index] - uri = tank.lnnode.getURI() - res = self.lncli(f"connect {uri}") - return res - - @abstractmethod - def generate_cli_command(self, command: list[str]): - pass - - @abstractmethod - def export(self, config: object, tar_file): - pass - - -def lnd_to_cl_scid(id) -> str: - s = int(id, 10) - block = s >> 40 - tx = s >> 16 & 0xFFFFFF - output = s & 0xFFFF - return f"{block}x{tx}x{output}" - - -def cl_to_lnd_scid(s) -> int: - s = [int(i) for i in s.split("x")] - return (s[0] << 40) | (s[1] << 16) | s[2] diff --git a/src/warnet/logging_config.json b/src/warnet/logging_config.json index 9ab9cabca..3aec3ad34 100644 --- a/src/warnet/logging_config.json +++ b/src/warnet/logging_config.json @@ -11,17 +11,11 @@ "datefmt": "%Y-%m-%d %H:%M:%S" } }, - "filters": { - "no_errors": { - "()": "warnet.utils.NonErrorFilter" - } - }, "handlers": { "stdout": { "class": "logging.StreamHandler", "level": "DEBUG", "formatter": "simple", - "filters": ["no_errors"], "stream": "ext://sys.stdout" }, "stderr": { diff --git a/src/warnet/main.py b/src/warnet/main.py new file mode 100644 index 000000000..768a82f96 --- /dev/null +++ b/src/warnet/main.py @@ -0,0 +1,86 @@ +import click + +from .admin import admin +from .bitcoin import bitcoin +from .control import down, logs, run, snapshot, stop +from .dashboard import dashboard +from .deploy import deploy +from .graph import create, graph, import_network +from .image import image +from .ln import ln +from .project import init, new, setup +from .status import status +from .users import auth + + +@click.group() +def cli(): + pass + + +@click.command() +def version() -> None: + """Display the installed version of warnet""" + try: + from warnet._version import __version__ + + # For PyPI releases, this will be the exact tag (e.g. "1.1.11") + # For dev installs, it will be something like "1.1.11.post1.dev17+g27af3a7.d20250309" + # Which is .post.dev+g.d + # is the number of local commits since the checkout commit + # is the number of commits since the last tag + raw_version = __version__ + + # Format the version string to our desired format + if "+" in raw_version: + version_part, git_date_part = raw_version.split("+", 1) + + # Get just the git commit hash + commit_hash = ( + git_date_part[1:].split(".", 1)[0] + if git_date_part.startswith("g") + else git_date_part.split(".", 1)[0] + ) + + # Remove .dev component (from "no-guess-dev" scheme) + clean_version = version_part + if ".dev" in clean_version: + clean_version = clean_version.split(".dev")[0] + + # Apply dirty status (from "no-guess-dev" scheme) + if ".post" in clean_version: + base = clean_version.split(".post")[0] + version_str = f"{base}-{commit_hash}-dirty" + else: + version_str = f"{clean_version}-{commit_hash}" + else: + version_str = raw_version + + click.echo(f"warnet version {version_str}") + except ImportError: + click.echo("warnet version unknown") + + +cli.add_command(admin) +cli.add_command(auth) +cli.add_command(bitcoin) +cli.add_command(deploy) +cli.add_command(down) +cli.add_command(dashboard) +cli.add_command(graph) +cli.add_command(import_network) +cli.add_command(image) +cli.add_command(init) +cli.add_command(logs) +cli.add_command(ln) +cli.add_command(new) +cli.add_command(run) +cli.add_command(setup) +cli.add_command(snapshot) +cli.add_command(status) +cli.add_command(stop) +cli.add_command(create) +cli.add_command(version) + +if __name__ == "__main__": + cli() diff --git a/src/warnet/namespaces.py b/src/warnet/namespaces.py new file mode 100644 index 000000000..12357525b --- /dev/null +++ b/src/warnet/namespaces.py @@ -0,0 +1,90 @@ +import shutil +from pathlib import Path + +import click + +from .constants import ( + DEFAULT_NAMESPACES, + DEFAULTS_NAMESPACE_FILE, + NAMESPACES_DIR, + NAMESPACES_FILE, + WARGAMES_NAMESPACE_PREFIX, +) +from .process import run_command, stream_command + + +def copy_namespaces_defaults(directory: Path): + """Create the project structure for a warnet project""" + (directory / NAMESPACES_DIR.name / DEFAULT_NAMESPACES).mkdir(parents=True, exist_ok=True) + target_namespaces_defaults = ( + directory / NAMESPACES_DIR.name / DEFAULT_NAMESPACES / DEFAULTS_NAMESPACE_FILE + ) + target_namespaces_example = ( + directory / NAMESPACES_DIR.name / DEFAULT_NAMESPACES / NAMESPACES_FILE + ) + shutil.copy2( + NAMESPACES_DIR / DEFAULT_NAMESPACES / DEFAULTS_NAMESPACE_FILE, target_namespaces_defaults + ) + shutil.copy2(NAMESPACES_DIR / DEFAULT_NAMESPACES / NAMESPACES_FILE, target_namespaces_example) + + +@click.group(name="namespaces") +def namespaces(): + """Namespaces commands""" + + +@namespaces.command() +def list(): + """List all namespaces with 'wargames-' prefix""" + cmd = "kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'" + res = run_command(cmd) + all_namespaces = res.split() + warnet_namespaces = [ + ns for ns in all_namespaces if ns.startswith(f"{WARGAMES_NAMESPACE_PREFIX}") + ] + + if warnet_namespaces: + print("Warnet namespaces:") + for ns in warnet_namespaces: + print(f"- {ns}") + else: + print("No warnet namespaces found.") + + +@namespaces.command() +@click.option("--all", "destroy_all", is_flag=True, help="Destroy all warnet- prefixed namespaces") +@click.argument("namespace", required=False) +def destroy(destroy_all: bool, namespace: str): + """Destroy a specific namespace or all 'wargames-' prefixed namespaces""" + if destroy_all: + cmd = "kubectl get namespaces -o jsonpath='{.items[*].metadata.name}'" + res = run_command(cmd) + + # Get the list of namespaces + all_namespaces = res.split() + warnet_namespaces = [ + ns for ns in all_namespaces if ns.startswith(f"{WARGAMES_NAMESPACE_PREFIX}") + ] + + if not warnet_namespaces: + print("No warnet namespaces found to destroy.") + return + + for ns in warnet_namespaces: + destroy_cmd = f"kubectl delete namespace {ns}" + if not stream_command(destroy_cmd): + print(f"Failed to destroy namespace: {ns}") + else: + print(f"Destroyed namespace: {ns}") + elif namespace: + if not namespace.startswith(f"{WARGAMES_NAMESPACE_PREFIX}"): + print(f"Error: Can only destroy namespaces with '{WARGAMES_NAMESPACE_PREFIX}' prefix") + return + + destroy_cmd = f"kubectl delete namespace {namespace}" + if not stream_command(destroy_cmd): + print(f"Failed to destroy namespace: {namespace}") + else: + print(f"Destroyed namespace: {namespace}") + else: + print("Error: Please specify a namespace or use --all flag.") diff --git a/src/warnet/network.py b/src/warnet/network.py new file mode 100644 index 000000000..920269f00 --- /dev/null +++ b/src/warnet/network.py @@ -0,0 +1,92 @@ +import json +import shutil +from pathlib import Path + +from rich import print + +from .bitcoin import _rpc +from .constants import ( + NETWORK_DIR, + PLUGINS_DIR, + SCENARIOS_DIR, +) +from .k8s import get_mission + + +def copy_defaults(directory: Path, target_subdir: str, source_path: Path, exclude_list: list[str]): + """Generic function to copy default files and directories""" + target_dir = directory / target_subdir + target_dir.mkdir(parents=True, exist_ok=True) + print(f"Creating directory: {target_dir}") + + shutil.copytree( + src=source_path, + dst=target_dir, + dirs_exist_ok=True, + ignore=shutil.ignore_patterns(*exclude_list), + ) + + print(f"Finished copying files to {target_dir}") + + +def copy_network_defaults(directory: Path): + """Create the project structure for a warnet project's network""" + copy_defaults( + directory, + NETWORK_DIR.name, + NETWORK_DIR, + ["__pycache__", "__init__.py"], + ) + + +def copy_scenario_defaults(directory: Path): + """Create the project structure for a warnet project's scenarios""" + copy_defaults( + directory, + SCENARIOS_DIR.name, + SCENARIOS_DIR, + ["__pycache__", "test_scenarios"], + ) + + +def copy_plugins_defaults(directory: Path): + """Create the project structure for a warnet project's scenarios""" + copy_defaults( + directory, + PLUGINS_DIR.name, + PLUGINS_DIR, + ["__pycache__", "__init__"], + ) + + +def is_connection_manual(peer): + # newer nodes specify a "connection_type" + return bool(peer.get("connection_type") == "manual" or peer.get("addnode") is True) + + +def _connected(end="\n"): + tanks = get_mission("tank") + for tank in tanks: + # Get actual + try: + peerinfo = json.loads( + _rpc(tank.metadata.name, "getpeerinfo", "", namespace=tank.metadata.namespace) + ) + actual = 0 + for peer in peerinfo: + if is_connection_manual(peer): + actual += 1 + expected = int(tank.metadata.annotations["init_peers"]) + print( + f"Tank: {tank.metadata.name:<15} peers expected: {expected:<3} actual: {actual:<3}", + end=end, + ) + # Even if more edges are specified, bitcoind only allows + # 8 manual outbound connections + if min(8, expected) > actual: + print("\nNetwork not connected") + return False + except Exception: + return False + print("Network connected ") + return True diff --git a/src/warnet/process.py b/src/warnet/process.py new file mode 100644 index 000000000..626124b71 --- /dev/null +++ b/src/warnet/process.py @@ -0,0 +1,31 @@ +import subprocess + + +def run_command(command: str) -> str: + result = subprocess.run(command, shell=True, capture_output=True, text=True, executable="bash") + if result.returncode != 0: + raise Exception(result.stderr) + return result.stdout + + +def stream_command(command: str) -> bool: + process = subprocess.Popen( + ["bash", "-c", command], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + universal_newlines=True, + ) + + message = "" + for line in iter(process.stdout.readline, ""): + message += line + print(line, end="") + + process.stdout.close() + return_code = process.wait() + + if return_code != 0: + raise Exception(message) + return True diff --git a/src/warnet/project.py b/src/warnet/project.py new file mode 100644 index 000000000..6907c2bad --- /dev/null +++ b/src/warnet/project.py @@ -0,0 +1,630 @@ +import hashlib +import os +import platform +import shutil +import subprocess +import sys +import tarfile +import tempfile +from dataclasses import dataclass +from enum import Enum, auto +from pathlib import Path +from typing import Callable, Optional + +import click +import inquirer +import requests + +from .constants import ( + HELM_BINARY_NAME, + HELM_BLESSED_NAME_AND_CHECKSUMS, + HELM_BLESSED_VERSION, + HELM_DOWNLOAD_URL_STUB, + KUBECTL_BINARY_NAME, + KUBECTL_BLESSED_NAME_AND_CHECKSUMS, + KUBECTL_BLESSED_VERSION, + KUBECTL_DOWNLOAD_URL_STUB, +) +from .graph import inquirer_create_network +from .network import copy_network_defaults, copy_plugins_defaults, copy_scenario_defaults + + +@click.command() +def setup(): + """Setup warnet""" + + class ToolStatus(Enum): + Satisfied = auto() + Unsatisfied = auto() + + @dataclass + class ToolInfo: + tool_name: str + is_installed_func: Callable[[], tuple[bool, str]] + install_instruction: str + install_url: str + + __slots__ = ["tool_name", "is_installed_func", "install_instruction", "install_url"] + + def is_minikube_installed() -> tuple[bool, str]: + try: + version_result = subprocess.run( + ["minikube", "version", "--short"], + capture_output=True, + text=True, + ) + location_result = subprocess.run( + ["which", "minikube"], + capture_output=True, + text=True, + ) + if version_result.returncode == 0 and location_result.returncode == 0: + return True, location_result.stdout.strip() + else: + return False, "" + except FileNotFoundError as err: + return False, str(err) + + def is_minikube_running() -> tuple[bool, str]: + try: + result = subprocess.run( + ["minikube", "status"], + capture_output=True, + text=True, + ) + if result.returncode == 0 and "Running" in result.stdout: + return True, "minikube is running" + else: + return False, "" + except FileNotFoundError: + # Minikube command not found + return False, "" + + def is_docker_running() -> tuple[bool, str]: + try: + result = subprocess.run( + ["docker", "info"], + capture_output=True, + text=True, + ) + if result.returncode == 0: + return True, "docker is running" + else: + return False, "" + except FileNotFoundError: + # Docker command not found + return False, "" + + def is_minikube_version_valid_on_darwin() -> tuple[bool, str]: + try: + version_result = subprocess.run( + ["minikube", "version", "--short"], + capture_output=True, + text=True, + ) + location_result = subprocess.run( + ["which", "minikube"], + capture_output=True, + text=True, + ) + if version_result.returncode == 0 and location_result.returncode == 0: + version = version_result.stdout.strip().split()[-1] # Get the version number + return version not in [ + "v1.32.0", + "1.33.0", + ], f"{location_result.stdout.strip()} ({version})" + else: + return False, "" + except FileNotFoundError as err: + return False, str(err) + + def is_platform_darwin() -> bool: + return platform.system() == "Darwin" + + def is_docker_installed() -> tuple[bool, str]: + try: + version_result = subprocess.run(["docker", "--version"], capture_output=True, text=True) + location_result = subprocess.run( + ["which", "docker"], + capture_output=True, + text=True, + ) + if version_result.returncode == 0 and location_result.returncode == 0: + return True, location_result.stdout.strip() + else: + return False, "" + except FileNotFoundError as err: + return False, str(err) + + def is_docker_desktop_running() -> tuple[bool, str]: + try: + version_result = subprocess.run(["docker", "info"], capture_output=True, text=True) + location_result = subprocess.run( + ["which", "docker"], + capture_output=True, + text=True, + ) + if version_result.returncode == 0 and location_result.returncode == 0: + return "Docker Desktop" in version_result.stdout, location_result.stdout.strip() + else: + return False, "" + except FileNotFoundError as err: + return False, str(err) + + def is_docker_desktop_kube_running() -> tuple[bool, str]: + try: + cluster_info = subprocess.run( + ["kubectl", "cluster-info", "--request-timeout=1"], + capture_output=True, + text=True, + ) + if cluster_info.returncode == 0: + indented_output = cluster_info.stdout.strip().replace("\n", "\n\t") + return True, f"\n\t{indented_output}" + else: + return False, "" + except Exception: + print() + return False, "Please enable kubernetes in Docker Desktop" + + def is_kubectl_installed_and_offer_if_not() -> tuple[bool, str]: + try: + version_result = subprocess.run( + ["kubectl", "version", "--client"], + capture_output=True, + text=True, + ) + location_result = subprocess.run( + ["which", "kubectl"], + capture_output=True, + text=True, + ) + if version_result.returncode == 0 and location_result.returncode == 0: + return True, location_result.stdout.strip() + else: + return False, "" + except FileNotFoundError: + print() + kubectl_answer = inquirer.prompt( + [ + inquirer.Confirm( + "install_kubectl", + message=click.style( + "Would you like Warnet to install Kubectl into your virtual environment?", + fg="blue", + bold=True, + ), + default=True, + ), + ] + ) + if kubectl_answer is None: + msg = "Setup cancelled by user." + click.secho(msg, fg="yellow") + return False, msg + if kubectl_answer["install_kubectl"]: + click.secho(" Installing Kubectl...", fg="yellow", bold=True) + install_kubectl_rootlessly_to_venv() + return is_kubectl_installed_and_offer_if_not() + return False, "Please install Kubectl." + + def is_helm_installed_and_offer_if_not() -> tuple[bool, str]: + try: + version_result = subprocess.run(["helm", "version"], capture_output=True, text=True) + location_result = subprocess.run( + ["which", "helm"], + capture_output=True, + text=True, + ) + if version_result.returncode == 0 and location_result.returncode == 0: + return version_result.returncode == 0, location_result.stdout.strip() + else: + return False, "" + + except FileNotFoundError: + print() + helm_answer = inquirer.prompt( + [ + inquirer.Confirm( + "install_helm", + message=click.style( + "Would you like Warnet to install Helm into your virtual environment?", + fg="blue", + bold=True, + ), + default=True, + ), + ] + ) + if helm_answer is None: + msg = "Setup cancelled by user." + click.secho(msg, fg="yellow") + return False, msg + if helm_answer["install_helm"]: + click.secho(" Installing Helm...", fg="yellow", bold=True) + install_helm_rootlessly_to_venv() + return is_helm_installed_and_offer_if_not() + return False, "Please install Helm." + + def check_installation(tool_info: ToolInfo) -> ToolStatus: + has_good_version, location = tool_info.is_installed_func() + if not has_good_version: + instruction_label = click.style(" Instruction: ", fg="yellow", bold=True) + instruction_text = click.style(f"{tool_info.install_instruction}", fg="yellow") + url_label = click.style(" URL: ", fg="yellow", bold=True) + url_text = click.style(f"{tool_info.install_url}", fg="yellow") + + click.secho(f" 💥 {tool_info.tool_name} is not satisfied. {location}", fg="yellow") + click.echo(instruction_label + instruction_text) + click.echo(url_label + url_text) + return ToolStatus.Unsatisfied + else: + click.secho(f" ⭐️ {tool_info.tool_name} is satisfied: {location}", bold=False) + return ToolStatus.Satisfied + + docker_info = ToolInfo( + tool_name="Docker", + is_installed_func=is_docker_installed, + install_instruction="Install Docker from Docker's official site.", + install_url="https://docs.docker.com/engine/install/", + ) + docker_desktop_info = ToolInfo( + tool_name="Docker Desktop", + is_installed_func=is_docker_desktop_running, + install_instruction="Make sure Docker Desktop is installed and running.", + install_url="https://docs.docker.com/desktop/", + ) + docker_running_info = ToolInfo( + tool_name="Running Docker", + is_installed_func=is_docker_running, + install_instruction="Please make sure docker is running", + install_url="https://docs.docker.com/engine/install/", + ) + docker_desktop_kube_running = ToolInfo( + tool_name="Kubernetes Running in Docker Desktop", + is_installed_func=is_docker_desktop_kube_running, + install_instruction="Please enable the local kubernetes cluster in Docker Desktop", + install_url="https://docs.docker.com/desktop/kubernetes/", + ) + minikube_running_info = ToolInfo( + tool_name="Running Minikube", + is_installed_func=is_minikube_running, + install_instruction="Please make sure minikube is running", + install_url="https://minikube.sigs.k8s.io/docs/start/", + ) + kubectl_info = ToolInfo( + tool_name="Kubectl", + is_installed_func=is_kubectl_installed_and_offer_if_not, + install_instruction="Install kubectl.", + install_url="https://kubernetes.io/docs/tasks/tools/install-kubectl/", + ) + helm_info = ToolInfo( + tool_name="Helm", + is_installed_func=is_helm_installed_and_offer_if_not, + install_instruction="Install Helm from Helm's official site, or rootlessly install Helm using Warnet's downloader when prompted.", + install_url="https://helm.sh/docs/intro/install/", + ) + minikube_info = ToolInfo( + tool_name="Minikube", + is_installed_func=is_minikube_installed, + install_instruction="Install Minikube from the official Minikube site.", + install_url="https://minikube.sigs.k8s.io/docs/start/", + ) + minikube_version_info = ToolInfo( + tool_name="Minikube's version", + is_installed_func=is_minikube_version_valid_on_darwin, + install_instruction="Install the latest Minikube from the official Minikube site.", + install_url="https://minikube.sigs.k8s.io/docs/start/", + ) + + print(" ") + print(" ╭───────────────────────────╮ ") + print(" │ Welcome to Warnet Setup │ ") + print(" ╰───────────────────────────╯ ") + print(" ") + print(" Let's find out if your system has what it takes to run Warnet...") + print("") + + try: + questions = [ + inquirer.List( + "platform", + message=click.style("Which platform would you like to use?", fg="blue", bold=True), + choices=[ + "Minikube", + "Docker Desktop", + "No Backend (Interacting with remote cluster, see `warnet auth --help`)", + ], + ) + ] + answers = inquirer.prompt(questions) + + check_results: list[ToolStatus] = [] + if answers: + check_results.append(check_installation(kubectl_info)) + check_results.append(check_installation(helm_info)) + if answers["platform"] == "Docker Desktop": + check_results.append(check_installation(docker_info)) + check_results.append(check_installation(docker_desktop_info)) + check_results.append(check_installation(docker_running_info)) + check_results.append(check_installation(docker_desktop_kube_running)) + elif answers["platform"] == "Minikube": + check_results.append(check_installation(docker_info)) + check_results.append(check_installation(docker_running_info)) + check_results.append(check_installation(minikube_info)) + if is_platform_darwin(): + check_results.append(check_installation(minikube_version_info)) + check_results.append(check_installation(minikube_running_info)) + else: + click.secho("Please re-run setup.", fg="yellow") + sys.exit(1) + + if ToolStatus.Unsatisfied in check_results: + click.secho( + "Please fix the installation issues above and try setup again.", fg="yellow" + ) + sys.exit(1) + else: + click.secho(" ⭐️ Warnet prerequisites look good.\n") + + except Exception as e: + click.echo(f"{e}\n\n") + click.secho(f"An error occurred while running the quick start script:\n\n{e}\n\n", fg="red") + click.secho( + "Please report the above context to https://github.com/bitcoin-dev-project/warnet/issues", + fg="yellow", + ) + return False + + +def create_warnet_project(directory: Path, check_empty: bool = False): + """Common function to create a warnet project""" + if check_empty and any(directory.iterdir()): + click.secho(f"Warning: Directory {directory} is not empty", fg="yellow") + if not click.confirm("Do you want to continue?", default=True): + return + + try: + copy_network_defaults(directory) + copy_scenario_defaults(directory) + copy_plugins_defaults(directory) + click.echo(f"Created warnet project structure in {directory}") + except Exception as e: + click.secho(f"Error creating project: {e}", fg="red") + raise e + + +@click.command() +@click.argument( + "directory", type=click.Path(file_okay=False, dir_okay=True, resolve_path=True, path_type=Path) +) +def new(directory: Path): + """Create a new warnet project in the specified directory""" + new_internal(directory) + + +def new_internal(directory: Path, from_init=False): + if directory.exists() and not from_init: + click.secho(f"Error: Directory {directory} already exists", fg="red") + return + + click.secho("\nCreating project structure...", fg="yellow", bold=True) + project_path = Path(os.path.expanduser(directory)) + create_warnet_project(project_path) + + custom_network_path = "" + click.secho("\nGenerating custom network...", fg="yellow", bold=True) + custom_network_path = inquirer_create_network(directory) + + if custom_network_path: + click.echo( + f"\nEdit the network files found under {custom_network_path}/ before deployment if you want to customise the network." + ) + click.echo("\nWhen you're ready, run the following command to deploy this network:") + click.echo(f" warnet deploy {custom_network_path}") + + +@click.command() +def init(): + """Initialize a warnet project in the current directory""" + current_dir = Path.cwd() + new_internal(directory=current_dir, from_init=True) + + +def get_os_name_for_helm() -> Optional[str]: + """Return a short operating system name suitable for downloading a helm binary.""" + uname_sys = platform.system().lower() + if "linux" in uname_sys: + return "linux" + elif uname_sys == "darwin": + return "darwin" + elif "win" in uname_sys: + return "windows" + return None + + +def is_in_virtualenv() -> bool: + """Check if the user is in a virtual environment.""" + return hasattr(sys, "real_prefix") or ( + hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix + ) + + +def download_file(url, destination): + click.secho(f" Downloading {url}", fg="blue") + response = requests.get(url, stream=True) + if response.status_code == 200: + with open(destination, "wb") as f: + for chunk in response.iter_content(1024): + f.write(chunk) + else: + raise Exception(f"Failed to download {url} (status code {response.status_code})") + + +def query_arch_from_uname(arch: str) -> Optional[str]: + if arch.startswith("armv5"): + return "armv5" + elif arch.startswith("armv6"): + return "armv6" + elif arch.startswith("armv7"): + return "arm" + elif arch == "aarch64" or arch == "arm64": + return "arm64" + elif arch == "x86": + return "386" + elif arch == "x86_64": + return "amd64" + elif arch == "i686" or arch == "i386": + return "386" + else: + return None + + +def write_blessed_kubectl_checksum(system: str, arch: str, dest_path: str): + checksum = next( + ( + b["checksum"] + for b in KUBECTL_BLESSED_NAME_AND_CHECKSUMS + if b["system"] == system and b["arch"] == arch + ), + None, + ) + if checksum: + with open(dest_path, "w") as f: + f.write(checksum) + else: + click.secho("Could not find a matching kubectl binary and checksum", fg="red") + + +def write_blessed_helm_checksum(helm_filename: str, dest_path: str): + checksum = next( + (b["checksum"] for b in HELM_BLESSED_NAME_AND_CHECKSUMS if b["name"] == helm_filename), None + ) + if checksum: + with open(dest_path, "w") as f: + f.write(checksum) + else: + click.secho("Could not find a matching helm binary and checksum", fg="red") + + +def verify_checksum(file_path, checksum_path): + click.secho(" Verifying checksum...", fg="blue") + sha256_hash = hashlib.sha256() + with open(file_path, "rb") as f: + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + + with open(checksum_path) as f: + expected_checksum = f.read().strip() + + if sha256_hash.hexdigest() != expected_checksum: + raise Exception("Checksum verification failed!") + click.secho(" Checksum verified.", fg="blue") + + +def install_to_venv(bin_path, binary_name): + venv_bin_dir = os.path.join(sys.prefix, "bin") + dst_path = os.path.join(venv_bin_dir, binary_name) + shutil.move(bin_path, dst_path) + os.chmod(dst_path, 0o755) + click.secho(f" {binary_name} installed into {dst_path}", fg="blue") + + +def install_helm_rootlessly_to_venv(): + if not is_in_virtualenv(): + click.secho( + "Error: You are not in a virtual environment. Please activate a virtual environment and try again.", + fg="yellow", + ) + sys.exit(1) + + version = HELM_BLESSED_VERSION + + os_name = get_os_name_for_helm() + if os_name is None: + click.secho( + "Error: Could not determine the operating system of this computer.", fg="yellow" + ) + sys.exit(1) + + uname_arch = os.uname().machine + arch = query_arch_from_uname(uname_arch) + if not arch: + click.secho(f"No Helm binary candidate for arch: {uname_arch}", fg="red") + sys.exit(1) + + helm_filename = f"{HELM_BINARY_NAME}-{version}-{os_name}-{arch}.tar.gz" + helm_url = f"{HELM_DOWNLOAD_URL_STUB}{helm_filename}" + + try: + with tempfile.TemporaryDirectory() as temp_dir: + helm_archive_path = os.path.join(temp_dir, helm_filename) + checksum_path = os.path.join(temp_dir, f"{helm_filename}.sha256") + + download_file(helm_url, helm_archive_path) + write_blessed_helm_checksum(helm_filename, checksum_path) + verify_checksum(helm_archive_path, checksum_path) + + # Extract Helm and install it in the virtual environment's bin folder + with tarfile.open(helm_archive_path, "r:gz") as tar: + tar.extractall(path=temp_dir) + helm_bin_path = os.path.join(temp_dir, os_name + "-" + arch, HELM_BINARY_NAME) + install_to_venv(helm_bin_path, HELM_BINARY_NAME) + + click.secho( + f" {HELM_BINARY_NAME} {version} installed successfully to your virtual environment!\n", + fg="blue", + ) + + except Exception as e: + click.secho(f"Error: {e}\nCould not install helm.", fg="yellow") + sys.exit(1) + + +def install_kubectl_rootlessly_to_venv(): + if not is_in_virtualenv(): + click.secho( + "Error: You are not in a virtual environment. Please activate a virtual environment and try again.", + fg="yellow", + ) + sys.exit(1) + + os_name = get_os_name_for_helm() + if os_name is None: + click.secho( + "Error: Could not determine the operating system of this computer.", fg="yellow" + ) + sys.exit(1) + + uname_arch = os.uname().machine + arch = query_arch_from_uname(uname_arch) + if arch not in ["arm64", "amd64"]: + click.secho(f"No Kubectl binary candidate for arch: {uname_arch}", fg="red") + sys.exit(1) + + uname_sys = os.uname().sysname.lower() + if uname_sys not in ["linux", "darwin"]: + click.secho(f"The following system is not supported: {uname_sys}", fg="red") + sys.exit(1) + + kubectl_url = f"{KUBECTL_DOWNLOAD_URL_STUB}/{uname_sys}/{arch}/{KUBECTL_BINARY_NAME}" + + try: + with tempfile.TemporaryDirectory() as temp_dir: + binary_path = os.path.join(temp_dir, KUBECTL_BINARY_NAME) + checksum_path = os.path.join(temp_dir, f"{KUBECTL_BINARY_NAME}.sha256") + + download_file(kubectl_url, binary_path) + write_blessed_kubectl_checksum(uname_sys, arch, checksum_path) + verify_checksum(binary_path, checksum_path) + + install_to_venv(binary_path, KUBECTL_BINARY_NAME) + + click.secho( + f" {KUBECTL_BINARY_NAME} {KUBECTL_BLESSED_VERSION} installed successfully to your virtual environment!\n", + fg="blue", + ) + + except Exception as e: + click.secho(f"Error: {e}\nCould not install helm.", fg="yellow") + sys.exit(1) diff --git a/src/warnet/scenarios/ln_init.py b/src/warnet/scenarios/ln_init.py deleted file mode 100644 index 98d8179df..000000000 --- a/src/warnet/scenarios/ln_init.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env python3 - -from time import sleep - -from warnet.scenarios.utils import ensure_miner -from warnet.test_framework_bridge import WarnetTestFramework - - -def cli_help(): - return "Fund LN wallets and open channels" - - -class LNInit(WarnetTestFramework): - def set_test_params(self): - self.num_nodes = None - - def run_test(self): - self.log.info("Lock out of IBD") - miner = ensure_miner(self.nodes[0]) - miner_addr = miner.getnewaddress() - self.generatetoaddress(self.nodes[0], 1, miner_addr) - - self.log.info("Get LN nodes and wallet addresses") - ln_nodes = [] - recv_addrs = [] - for tank in self.warnet.tanks: - if tank.lnnode is not None: - recv_addrs.append(tank.lnnode.getnewaddress()) - ln_nodes.append(tank.index) - - self.log.info("Fund LN wallets") - miner = ensure_miner(self.nodes[0]) - miner_addr = miner.getnewaddress() - # 298 block base - self.generatetoaddress(self.nodes[0], 297, miner_addr) - # divvy up the goods - split = (miner.getbalance() - 1) // len(recv_addrs) - sends = {} - for addr in recv_addrs: - sends[addr] = split - miner.sendmany("", sends) - # confirm funds in block 299 - self.generatetoaddress(self.nodes[0], 1, miner_addr) - - self.log.info( - f"Waiting for funds to be spendable: {split} BTC each for {len(recv_addrs)} LN nodes" - ) - - def funded_lnnodes(): - for tank in self.warnet.tanks: - if tank.lnnode is None: - continue - if int(tank.lnnode.get_wallet_balance()) < (split * 100000000): - return False - return True - - self.wait_until(funded_lnnodes, timeout=5 * 60) - - ln_nodes_uri = ln_nodes.copy() - while len(ln_nodes_uri) > 0: - self.log.info( - f"Waiting for all LN nodes to have URI, LN nodes remaining: {ln_nodes_uri}" - ) - for index in ln_nodes_uri: - lnnode = self.warnet.tanks[index].lnnode - if lnnode.getURI(): - ln_nodes_uri.remove(index) - sleep(5) - - self.log.info("Adding p2p connections to LN nodes") - for edge in self.warnet.graph.edges(data=True): - (src, dst, data) = edge - # Copy the L1 p2p topology (where applicable) to L2 - # so we get a more robust p2p graph for lightning - if ( - "channel_open" not in data - and self.warnet.tanks[src].lnnode - and self.warnet.tanks[dst].lnnode - ): - self.warnet.tanks[src].lnnode.connect_to_tank(dst) - - # Start confirming channel opens in block 300 - self.log.info("Opening channels, one per block") - chan_opens = [] - edges = self.warnet.graph.edges(data=True, keys=True) - edges = sorted(edges, key=lambda edge: edge[2]) - for edge in edges: - (src, dst, key, data) = edge - if "channel_open" in data: - src_node = self.warnet.get_ln_node_from_tank(src) - assert src_node is not None - assert self.warnet.get_ln_node_from_tank(dst) is not None - self.log.info(f"opening channel {src}->{dst}") - chan_pt = src_node.open_channel_to_tank(dst, data["channel_open"]) - # We can guarantee deterministic short channel IDs as long as - # the change output is greater than the channel funding output, - # which will then be output 0 - assert chan_pt[64:] == ":0" - chan_opens.append((edge, chan_pt)) - self.log.info(f" pending channel point: {chan_pt}") - self.wait_until( - lambda chan_pt=chan_pt: chan_pt[:64] in self.nodes[0].getrawmempool() - ) - self.generatetoaddress(self.nodes[0], 1, miner_addr) - assert chan_pt[:64] not in self.nodes[0].getrawmempool() - height = self.nodes[0].getblockcount() - self.log.info(f" confirmed in block {height}") - self.log.info( - f" channel_id should be: {int.from_bytes(height.to_bytes(3, 'big') + (1).to_bytes(3, 'big') + (0).to_bytes(2, 'big'), 'big')}" - ) - - # Ensure all channel opens are sufficiently confirmed - self.generatetoaddress(self.nodes[0], 10, miner_addr) - ln_nodes_gossip = ln_nodes.copy() - while len(ln_nodes_gossip) > 0: - self.log.info(f"Waiting for graph gossip sync, LN nodes remaining: {ln_nodes_gossip}") - for index in ln_nodes_gossip: - lnnode = self.warnet.tanks[index].lnnode - count_channels = len(lnnode.get_graph_channels()) - count_graph_nodes = len(lnnode.get_graph_nodes()) - if count_channels == len(chan_opens) and count_graph_nodes == len(ln_nodes): - ln_nodes_gossip.remove(index) - else: - self.log.info( - f" node {index} not synced (channels: {count_channels}/{len(chan_opens)}, nodes: {count_graph_nodes}/{len(ln_nodes)})" - ) - sleep(5) - - self.log.info("Updating channel policies") - for edge, chan_pt in chan_opens: - (src, dst, key, data) = edge - if "target_policy" in data: - target_node = self.warnet.get_ln_node_from_tank(dst) - target_node.update_channel_policy(chan_pt, data["target_policy"]) - if "source_policy" in data: - source_node = self.warnet.get_ln_node_from_tank(src) - source_node.update_channel_policy(chan_pt, data["source_policy"]) - - while True: - self.log.info("Waiting for all channel policies to match") - score = 0 - for tank_index, me in enumerate(ln_nodes): - you = (tank_index + 1) % len(ln_nodes) - my_channels = self.warnet.tanks[me].lnnode.get_graph_channels() - your_channels = self.warnet.tanks[you].lnnode.get_graph_channels() - match = True - for _chan_index, my_chan in enumerate(my_channels): - your_chan = [ - chan - for chan in your_channels - if chan.short_chan_id == my_chan.short_chan_id - ][0] - if not your_chan: - print(f"Channel policy missing for channel: {my_chan.short_chan_id}") - match = False - break - - try: - if not my_chan.channel_match(your_chan): - print( - f"Channel policy doesn't match between tanks {me} & {you}: {my_chan.short_chan_id}" - ) - match = False - break - except Exception as e: - print(f"Error comparing channel policies: {e}") - print( - f"Channel policy doesn't match between tanks {me} & {you}: {my_chan.short_chan_id}" - ) - match = False - break - if match: - print(f"All channel policies match between tanks {me} & {you}") - score += 1 - print(f"Score: {score} / {len(ln_nodes)}") - if score == len(ln_nodes): - break - sleep(5) - - self.log.info( - f"Warnet LN ready with {len(recv_addrs)} nodes and {len(chan_opens)} channels." - ) - - -if __name__ == "__main__": - LNInit().main() diff --git a/src/warnet/scenarios/sens_relay.py b/src/warnet/scenarios/sens_relay.py deleted file mode 100644 index 41cddf929..000000000 --- a/src/warnet/scenarios/sens_relay.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 - -from warnet.scenarios.utils import ensure_miner -from warnet.test_framework_bridge import WarnetTestFramework - - -def cli_help(): - return "Send a transaction using sensitive relay" - - -class MinerStd(WarnetTestFramework): - def set_test_params(self): - self.num_nodes = 12 - - def run_test(self): - # PR branch node - test_node = self.nodes[11] - test_wallet = ensure_miner(test_node) - addr = test_wallet.getnewaddress() - - self.log.info("generating 110 blocks...") - self.generatetoaddress(test_node, 110, addr) - - self.log.info("adding onion addresses from all peers...") - for i in range(11): - info = self.nodes[i].getnetworkinfo() - for addr in info["localaddresses"]: - if "onion" in addr["address"]: - self.log.info(f"adding {addr['address']}:{addr['port']}") - test_node.addpeeraddress(addr["address"], addr["port"]) - - self.log.info("getting address from recipient...") - # some other node - recip = self.nodes[5] - recip_wallet = ensure_miner(recip) - recip_addr = recip_wallet.getnewaddress() - - self.log.info("sending transaction...") - self.log.info(test_wallet.sendtoaddress(recip_addr, 0.5)) - - -if __name__ == "__main__": - MinerStd().main() diff --git a/src/warnet/scenarios/utils.py b/src/warnet/scenarios/utils.py deleted file mode 100644 index b0204d461..000000000 --- a/src/warnet/scenarios/utils.py +++ /dev/null @@ -1,5 +0,0 @@ -def ensure_miner(node): - wallets = node.listwallets() - if "miner" not in wallets: - node.createwallet("miner", descriptors=True) - return node.get_wallet_rpc("miner") diff --git a/src/warnet/server.py b/src/warnet/server.py deleted file mode 100644 index 55a7371db..000000000 --- a/src/warnet/server.py +++ /dev/null @@ -1,611 +0,0 @@ -import argparse -import base64 -import importlib -import io -import json -import logging -import logging.config -import os -import pkgutil -import platform -import shutil -import subprocess -import sys -import tarfile -import tempfile -import threading -import time -import traceback -from datetime import datetime - -import warnet.scenarios as scenarios -from flask import Flask, jsonify, request -from flask_jsonrpc.app import JSONRPC -from flask_jsonrpc.exceptions import ServerError -from warnet import SRC_DIR - -from .services import ServiceType -from .utils import gen_config_dir -from .warnet import Warnet - -WARNET_SERVER_PORT = 9276 -CONFIG_DIR_ALREADY_EXISTS = 32001 - - -class Server: - def __init__(self): - system = os.name - if system == "nt" or platform.system() == "Windows": - self.basedir = os.path.join(os.path.expanduser("~"), "warnet") - elif system == "posix" or platform.system() == "Linux" or platform.system() == "Darwin": - self.basedir = os.environ.get("XDG_STATE_HOME") - if self.basedir is None: - # ~/.warnet/warnet.log - self.basedir = os.path.join(os.environ["HOME"], ".warnet") - else: - # XDG_STATE_HOME / warnet / warnet.log - self.basedir = os.path.join(self.basedir, "warnet") - else: - raise NotImplementedError("Unsupported operating system") - - self.running_scenarios = [] - - self.app = Flask(__name__) - self.jsonrpc = JSONRPC(self.app, "/api") - - self.log_file_path = os.path.join(self.basedir, "warnet.log") - self.setup_global_exception_handler() - self.setup_logging() - self.setup_rpc() - self.warnets: dict = dict() - self.logger.info("Started server") - - # register a well known /-/healthy endpoint for liveness tests - # we regard warnet as healthy if the http server is up - # /-/healthy and /-/ready are often used (e.g. by the prometheus server) - self.app.add_url_rule("/-/healthy", view_func=self.healthy) - - # This is set while we bring a warnet up, which may include building a new image - # After warnet is up this will be released. - # This is used to delay api calls which rely on and image being built dynamically - # before the config dir is populated with the deployment info - self.image_build_lock = threading.Lock() - - def setup_global_exception_handler(self): - """ - Use flask to log traceback of unhandled exceptions - """ - - @self.app.errorhandler(Exception) - def handle_exception(e): - trace = traceback.format_exc() - self.logger.error(f"Unhandled exception: {e}\n{trace}") - response = { - "jsonrpc": "2.0", - "error": { - "code": -32603, - "message": "Internal server error", - "data": str(e), - }, - "id": request.json.get("id", None) if request.json else None, - } - return jsonify(response), 500 - - def healthy(self): - return "warnet is healthy" - - def setup_logging(self): - os.makedirs(os.path.dirname(self.log_file_path), exist_ok=True) - with open(SRC_DIR / "logging_config.json") as f: - logging_config = json.load(f) - logging_config["handlers"]["file"]["filename"] = str(self.log_file_path) - logging.config.dictConfig(logging_config) - self.logger = logging.getLogger("server") - self.scenario_logger = logging.getLogger("scenario") - self.logger.info("Logging started") - - def log_request(): - if "healthy" in request.path: - return # No need to log all these - if not request.path.startswith("/api"): - self.logger.debug(request.path) - else: - self.logger.debug(request.json) - - def build_check(): - timeout = 600 - check_interval = 10 - time_elapsed = 0 - - while time_elapsed < timeout: - # Attempt to acquire the lock without blocking - lock_acquired = self.image_build_lock.acquire(blocking=False) - # If we get the lock, release it and continue - if lock_acquired: - self.image_build_lock.release() - return - # Otherwise wait before trying again - else: - time.sleep(check_interval) - time_elapsed += check_interval - - # If we've reached here, the lock wasn't acquired in time - raise Exception( - f"Failed to acquire the build lock within {timeout} seconds, aborting RPC." - ) - - self.app.before_request(log_request) - self.app.before_request(build_check) - - def setup_rpc(self): - # Tanks - self.jsonrpc.register(self.tank_bcli) - self.jsonrpc.register(self.tank_lncli) - self.jsonrpc.register(self.tank_debug_log) - self.jsonrpc.register(self.tank_messages) - self.jsonrpc.register(self.tank_ln_pub_key) - # Scenarios - self.jsonrpc.register(self.scenarios_available) - self.jsonrpc.register(self.scenarios_run) - self.jsonrpc.register(self.scenarios_run_file) - self.jsonrpc.register(self.scenarios_stop) - self.jsonrpc.register(self.scenarios_list_running) - # Networks - self.jsonrpc.register(self.network_up) - self.jsonrpc.register(self.network_from_file) - self.jsonrpc.register(self.network_down) - self.jsonrpc.register(self.network_info) - self.jsonrpc.register(self.network_status) - self.jsonrpc.register(self.network_connected) - self.jsonrpc.register(self.network_export) - # Debug - self.jsonrpc.register(self.generate_deployment) - self.jsonrpc.register(self.exec_run) - # Logs - self.jsonrpc.register(self.logs_grep) - - def scenario_log(self, proc): - while not proc.stdout and not proc.stderr: - time.sleep(0.1) - for line in proc.stdout: - self.scenario_logger.info(line.decode().rstrip()) - for line in proc.stderr: - self.scenario_logger.error(line.decode().rstrip()) - - def get_warnet(self, network: str) -> Warnet: - """ - Will get a warnet from the cache if it exists. - Otherwise it will create the network using from_network() and save it - to the cache before returning it. - """ - if network in self.warnets: - return self.warnets[network] - wn = Warnet.from_network(network) - if isinstance(wn, Warnet): - self.warnets[network] = wn - return wn - raise ServerError(f"Could not find warnet {network}") - - def tank_bcli( - self, node: int, method: str, params: list[str] | None = None, network: str = "warnet" - ) -> str: - """ - Call bitcoin-cli on in [network] - """ - wn = self.get_warnet(network) - try: - return wn.container_interface.get_bitcoin_cli(wn.tanks[node], method, params) - except Exception as e: - msg = f"Sever error calling bitcoin-cli {method}: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def tank_lncli(self, node: int, command: list[str], network: str = "warnet") -> str: - """ - Call lightning cli on in [network] - """ - wn = self.get_warnet(network) - try: - return wn.container_interface.ln_cli(wn.tanks[node], command) - except Exception as e: - msg = f"Error calling lncli: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def tank_ln_pub_key(self, node: int, network: str = "warnet") -> str: - """ - Get lightning pub key on in [network] - """ - wn = self.get_warnet(network) - try: - return wn.container_interface.ln_pub_key(wn.tanks[node]) - except Exception as e: - msg = f"Error getting pub key: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def tank_debug_log(self, network: str, node: int) -> str: - """ - Fetch the Bitcoin Core debug log from - """ - wn = Warnet.from_network(network) - try: - return wn.container_interface.get_bitcoin_debug_log(wn.tanks[node].index) - except Exception as e: - msg = f"Error fetching debug logs: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def tank_messages(self, network: str, node_a: int, node_b: int) -> str: - """ - Fetch messages sent between and . - """ - wn = self.get_warnet(network) - try: - messages = [ - msg - for msg in wn.container_interface.get_messages( - wn.tanks[node_a].index, wn.tanks[node_b].index, wn.bitcoin_network - ) - if msg is not None - ] - if not messages: - msg = f"No messages found between {node_a} and {node_b}" - self.logger.error(msg) - raise ServerError(message=msg) - - messages_str_list = [] - - for message in messages: - # Check if 'time' key exists and its value is a number - if not (message.get("time") and isinstance(message["time"], int | float)): - continue - - timestamp = datetime.utcfromtimestamp(message["time"] / 1e6).strftime( - "%Y-%m-%d %H:%M:%S" - ) - direction = ">>>" if message.get("outbound", False) else "<<<" - msgtype = message.get("msgtype", "") - body_dict = message.get("body", {}) - - if not isinstance(body_dict, dict): # messages will be in dict form - continue - - body_str = ", ".join(f"{key}: {value}" for key, value in body_dict.items()) - messages_str_list.append(f"{timestamp} {direction} {msgtype} {body_str}") - - result_str = "\n".join(messages_str_list) - - return result_str - - except Exception as e: - msg = f"Error fetching messages between nodes {node_a} and {node_b}: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def network_export(self, network: str, activity: str | None, exclude: list[int]) -> bool: - """ - Export all data for a simln container running on the network - """ - wn = self.get_warnet(network) - if "simln" not in wn.services: - raise Exception("No simln service in network") - - # JSON object that will eventually be written to simln config file - config = {"nodes": []} - if activity: - config["activity"] = json.loads(activity) - # In-memory file to build tar archive - tar_buffer = io.BytesIO() - with tarfile.open(fileobj=tar_buffer, mode="w") as tar_file: - # tank LN nodes add their credentials to tar archive - wn.export(config, tar_file, exclude=exclude) - # write config file - config_bytes = json.dumps(config).encode("utf-8") - config_stream = io.BytesIO(config_bytes) - tarinfo = tarfile.TarInfo(name="sim.json") - tarinfo.size = len(config_bytes) - tar_file.addfile(tarinfo=tarinfo, fileobj=config_stream) - - # Write the archive to the RPC server's config directory - source_file = wn.config_dir / "simln.tar" - with open(source_file, "wb") as output: - tar_buffer.seek(0) - output.write(tar_buffer.read()) - - # Copy the archive to the "emptydir" volume in the simln pod - wn.container_interface.write_service_config(source_file, "simln", "/simln/") - return True - - def scenarios_available(self) -> list[tuple]: - """ - List available scenarios in the Warnet Test Framework - """ - try: - scenario_list = [] - for s in pkgutil.iter_modules(scenarios.__path__): - module_name = f"warnet.scenarios.{s.name}" - try: - m = importlib.import_module(module_name) - if hasattr(m, "cli_help"): - scenario_list.append((s.name, m.cli_help())) - except ModuleNotFoundError as e: - print(f"Module not found: {module_name}, error: {e}") - raise - return scenario_list - except Exception as e: - msg = f"Error listing scenarios: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def _start_scenario( - self, - scenario_path: str, - scenario_name: str, - additional_args: list[str], - network: str, - ) -> str: - try: - run_cmd = [sys.executable, scenario_path] + additional_args + [f"--network={network}"] - self.logger.debug(f"Running {run_cmd}") - proc = subprocess.Popen( - run_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - t = threading.Thread(target=lambda: self.scenario_log(proc)) - t.daemon = True - t.start() - cmd = f"{scenario_name} {' '.join(additional_args)}".strip() - self.running_scenarios.append( - { - "pid": proc.pid, - "cmd": cmd, - "proc": proc, - "network": network, - } - ) - return f"Running scenario {scenario_name} with PID {proc.pid} in the background..." - except Exception as e: - msg = f"Error running scenario: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def scenarios_run_file( - self, - scenario_base64: str, - scenario_name: str, - additional_args: list[str], - network: str = "warnet", - ) -> str: - # Extract just the filename without path and extension - with tempfile.NamedTemporaryFile( - prefix=scenario_name, - suffix=".py", - delete=False, - ) as temp_file: - scenario_path = temp_file.name - temp_file.write(base64.b64decode(scenario_base64)) - - if not os.path.exists(scenario_path): - raise ServerError(f"Scenario not found at {scenario_path}.") - - return self._start_scenario(scenario_path, scenario_name, additional_args, network) - - def scenarios_run( - self, scenario: str, additional_args: list[str], network: str = "warnet" - ) -> str: - # Use importlib.resources to get the scenario path - scenario_package = "warnet.scenarios" - scenario_filename = f"{scenario}.py" - - # Ensure the scenario file exists within the package - with importlib.resources.path(scenario_package, scenario_filename) as scenario_path: - scenario_path = str(scenario_path) # Convert Path object to string - - if not os.path.exists(scenario_path): - raise ServerError(f"Scenario {scenario} not found at {scenario_path}.") - - return self._start_scenario(scenario_path, scenario, additional_args, network) - - def scenarios_stop(self, pid: int) -> str: - matching_scenarios = [sc for sc in self.running_scenarios if sc["pid"] == pid] - if matching_scenarios: - matching_scenarios[0]["proc"].terminate() # sends SIGTERM - # Remove from running list - self.running_scenarios = [sc for sc in self.running_scenarios if sc["pid"] != pid] - return f"Stopped scenario with PID {pid}." - else: - msg = f"Could not find scenario with PID {pid}" - self.logger.error(msg) - raise ServerError(message=msg) - - def scenarios_list_running(self) -> list[dict]: - running = [ - { - "pid": sc["pid"], - "cmd": sc["cmd"], - "active": sc["proc"].poll() is None, - "return_code": sc["proc"].returncode, - "network": sc["network"], - } - for sc in self.running_scenarios - ] - return running - - def network_up(self, network: str = "warnet") -> str: - def thread_start(server: Server, network): - try: - wn = server.get_warnet(network) - wn.apply_network_conditions() - wn.wait_for_health() - server.logger.info( - f"Successfully resumed warnet named '{network}' from config dir {wn.config_dir}" - ) - except Exception as e: - trace = traceback.format_exc() - server.logger.error(f"Unhandled exception bringing network up: {e}\n{trace}") - - try: - t = threading.Thread(target=lambda: thread_start(self, network)) - t.daemon = True - t.start() - return "Resuming warnet..." - except Exception as e: - msg = f"Error bring up warnet: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def network_from_file( - self, graph_file: str, force: bool = False, network: str = "warnet" - ) -> dict: - """ - Run a warnet with topology loaded from a - """ - - def thread_start(server: Server, network): - with server.image_build_lock: - try: - wn = server.get_warnet(network) - wn.generate_deployment() - wn.warnet_build() - wn.warnet_up() - wn.wait_for_health() - wn.apply_network_conditions() - self.logger.info("Warnet started successfully") - except Exception as e: - trace = traceback.format_exc() - self.logger.error(f"Unhandled exception starting warnet: {e}\n{trace}") - - config_dir = gen_config_dir(network) - if config_dir.exists(): - if force: - shutil.rmtree(config_dir) - else: - message = f"Config dir {config_dir} already exists, not overwriting existing warnet without --force" - self.logger.error(message) - raise ServerError(message=message, code=CONFIG_DIR_ALREADY_EXISTS) - - try: - self.warnets[network] = Warnet.from_graph_file( - graph_file, - config_dir, - network, - ) - t = threading.Thread(target=lambda: thread_start(self, network)) - t.daemon = True - t.start() - return self.warnets[network]._warnet_dict_representation() - except Exception as e: - msg = f"Error bring up warnet: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def network_down(self, network: str = "warnet") -> str: - """ - Stop all containers in . - """ - wn = self.get_warnet(network) - try: - wn.warnet_down() - return "Stopping warnet" - except Exception as e: - msg = f"Error bringing warnet down: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def network_info(self, network: str = "warnet") -> dict: - """ - Get info about a warnet network named - """ - wn = self.get_warnet(network) - return wn._warnet_dict_representation() - - def network_status(self, network: str = "warnet") -> list[dict]: - """ - Get running status of a warnet network named - """ - try: - wn = self.get_warnet(network) - stats = [] - for tank in wn.tanks: - status = {"tank_index": tank.index, "bitcoin_status": tank.status.name.lower()} - if tank.lnnode is not None: - status["lightning_status"] = tank.lnnode.status.name.lower() - if tank.lnnode.cb is not None: - status["circuitbreaker_status"] = tank.lnnode.cb_status.name.lower() - stats.append(status) - return stats - except Exception as e: - msg = f"Error getting network status: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def network_connected(self, network: str = "warnet") -> bool: - """ - Indicate whether all of the graph edges are connected in - """ - try: - wn = self.get_warnet(network) - return wn.network_connected() - except Exception as e: - self.logger.error(f"{e}") - return False - - def generate_deployment(self, graph_file: str, network: str = "warnet") -> str: - """ - Generate the deployment file for a graph file - """ - try: - config_dir = gen_config_dir(network) - if config_dir.exists(): - message = f"Config dir {config_dir} already exists, not overwriting existing warnet without --force" - self.logger.error(message) - raise ServerError(message=message, code=CONFIG_DIR_ALREADY_EXISTS) - wn = self.get_warnet(network) - wn.generate_deployment() - if not wn.deployment_file or not wn.deployment_file.is_file(): - raise ServerError(f"No deployment file found at {wn.deployment_file}") - with open(wn.deployment_file) as f: - return f.read() - except Exception as e: - msg = f"Error generating deployment file: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def logs_grep( - self, pattern: str, network: str = "warnet", k8s_timestamps=False, no_sort=False - ) -> str: - """ - Grep the logs from the fluentd container for a regex pattern - """ - try: - wn = self.get_warnet(network) - return wn.container_interface.logs_grep(pattern, network, k8s_timestamps, no_sort) - except Exception as e: - msg = f"Error grepping logs using pattern {pattern}: {e}" - self.logger.error(msg) - raise ServerError(message=msg) from e - - def exec_run(self, index: int, service_type: int, cmd: str, network: str = "warnet") -> str: - """ - Execute an arbitrary command in an arbitrary container, - identified by tank index and ServiceType - """ - wn = self.get_warnet(network) - return wn.container_interface.exec_run(index, ServiceType(service_type), cmd) - - -def run_server(): - parser = argparse.ArgumentParser(description="Run the server") - parser.add_argument( - "--dev", action="store_true", help="Run in development mode with debug enabled" - ) - args = parser.parse_args() - debug_mode = args.dev - server = Server() - server.app.run(host="0.0.0.0", port=WARNET_SERVER_PORT, debug=debug_mode) - - -if __name__ == "__main__": - run_server() diff --git a/src/warnet/services.py b/src/warnet/services.py deleted file mode 100644 index 562813432..000000000 --- a/src/warnet/services.py +++ /dev/null @@ -1,36 +0,0 @@ -from enum import Enum - -FO_CONF_NAME = "fork_observer_config.toml" -AO_CONF_NAME = "addrman_observer_config.toml" -GRAFANA_PROVISIONING = "grafana-provisioning" -PROM_CONF_NAME = "prometheus.yml" - - -class ServiceType(Enum): - BITCOIN = 1 - LIGHTNING = 2 - CIRCUITBREAKER = 3 - - -SERVICES = { - # "forkobserver": { - # "image": "b10c/fork-observer:latest", - # "container_name_suffix": "fork-observer", - # "warnet_port": "23001", - # "container_port": "2323", - # "config_files": [f"{FO_CONF_NAME}:/app/config.toml"], - # }, - # "addrmanobserver": { - # "image": "b10c/addrman-observer:latest", - # "container_name_suffix": "addrman-observer", - # "warnet_port": "23005", - # "container_port": "3882", - # "config_files": [f"{AO_CONF_NAME}:/app/config.toml"], - # }, - "simln": { - "image": "bitcoindevproject/simln:0.2.0", - "container_name_suffix": "simln", - "environment": ["LOG_LEVEL=debug", "SIMFILE_PATH=/simln/sim.json"], - "config_files": ["simln/:/simln"], - }, -} diff --git a/src/warnet/status.py b/src/warnet/status.py index ac83d4140..b50ff8680 100644 --- a/src/warnet/status.py +++ b/src/warnet/status.py @@ -1,9 +1,128 @@ -from enum import Enum +import sys +import click +from kubernetes.config.config_exception import ConfigException +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text +from urllib3.exceptions import MaxRetryError -class RunningStatus(Enum): - PENDING = 1 - RUNNING = 2 - STOPPED = 3 - FAILED = 4 - UNKNOWN = 5 +from .constants import COMMANDER_MISSION, LIGHTNING_MISSION, TANK_MISSION +from .k8s import get_mission +from .network import _connected + + +@click.command() +def status(): + """Display the unified status of the Warnet network and active scenarios""" + console = Console() + + try: + tanks = _get_tank_status() + lns = _get_ln_status() + scenarios = _get_deployed_scenarios() + except ConfigException as e: + print(e) + print( + "The kubeconfig file has not been properly set. This may mean that you need to " + "authorize with a cluster such as by starting minikube, starting docker-desktop, or " + "authorizing with a configuration file provided by a cluster administrator." + ) + sys.exit(1) + except MaxRetryError as e: + print(e) + print( + "Warnet cannot get the status of a Warnet network. To resolve this, you may need to " + "confirm you have access to a Warnet cluster. Start by checking your network " + "connection. Then, if running a local cluster, check that minikube or docker-desktop " + "is running properly. If you are trying to connect to a remote cluster, check that " + "the relevant authorization file has been configured properly as instructed by a " + "cluster administrator." + ) + sys.exit(1) + + # Create a unified table + table = Table(title="Warnet Status", show_header=True, header_style="bold magenta") + table.add_column("Component", style="cyan") + table.add_column("Name", style="green") + table.add_column("Status", style="yellow") + table.add_column("Namespace", style="green") + + # Add tanks to the table + for tank in tanks: + table.add_row("Tank", tank["name"], tank["status"], tank["namespace"]) + + # Add a separator if there are both tanks and scenarios + if tanks and lns: + table.add_row("", "", "") + + for ln in lns: + table.add_row("Lightning", ln["name"], ln["status"], ln["namespace"]) + + table.add_row("", "", "") + + # Add scenarios to the table + active = 0 + if scenarios: + for scenario in scenarios: + table.add_row("Scenario", scenario["name"], scenario["status"], scenario["namespace"]) + if scenario["status"] == "running" or scenario["status"] == "pending": + active += 1 + else: + table.add_row("", "No active scenarios", "", style="red") + + # Create a panel to wrap the table + panel = Panel( + table, + title="Warnet Overview", + expand=False, + border_style="blue", + padding=(1, 1), + ) + + # Print the panel + console.print(panel) + + # Print summary + summary = Text() + summary.append(f"\nTotal Tanks: {len(tanks)}", style="bold cyan") + summary.append(f" | Active Scenarios: {active}", style="bold green") + console.print(summary) + _connected(end="\r") + + +def _get_tank_status(): + tanks = get_mission(TANK_MISSION) + return [ + { + "name": tank.metadata.name, + "status": tank.status.phase.lower(), + "namespace": tank.metadata.namespace, + } + for tank in tanks + ] + + +def _get_ln_status(): + tanks = get_mission(LIGHTNING_MISSION) + return [ + { + "name": tank.metadata.name, + "status": tank.status.phase.lower(), + "namespace": tank.metadata.namespace, + } + for tank in tanks + ] + + +def _get_deployed_scenarios(): + commanders = get_mission(COMMANDER_MISSION) + return [ + { + "name": c.metadata.name, + "status": c.status.phase.lower(), + "namespace": c.metadata.namespace, + } + for c in commanders + ] diff --git a/src/warnet/tank.py b/src/warnet/tank.py deleted file mode 100644 index ac04d3f70..000000000 --- a/src/warnet/tank.py +++ /dev/null @@ -1,198 +0,0 @@ -""" -Tanks are containerized bitcoind nodes -""" - -import logging - -from .services import ServiceType -from .status import RunningStatus -from .utils import ( - SUPPORTED_TAGS, - exponential_backoff, - generate_ipv4_addr, - sanitize_tc_netem_command, -) - -CONTAINER_PREFIX_PROMETHEUS = "prometheus_exporter" - -logger = logging.getLogger("tank") - -CONFIG_BASE = " ".join( - [ - "-regtest=1", - "-checkmempool=0", - "-acceptnonstdtxn=1", - "-debuglogfile=0", - "-logips=1", - "-logtimemicros=1", - "-capturemessages=1", - "-rpcallowip=0.0.0.0/0", - "-rpcbind=0.0.0.0", - "-fallbackfee=0.00001000", - "-listen=1", - ] -) - - -class Tank: - DEFAULT_BUILD_ARGS = "--disable-tests --with-incompatible-bdb --without-gui --disable-bench --disable-fuzz-binary --enable-suppress-external-warnings --enable-debug " - - def __init__(self, index: int, warnet): - from warnet.lnnode import LNNode - - self.index = index - self.warnet = warnet - self.network_name = warnet.network_name - self.bitcoin_network = warnet.bitcoin_network - self.version: str = "" - self.image: str = "" - self.bitcoin_config = "" - self.netem = None - self.exporter = False - self.metrics = None - self.collect_logs = False - self.build_args = "" - self.lnnode: LNNode | None = None - self.rpc_port = 18443 - self.rpc_user = "warnet_user" - self.rpc_password = "2themoon" - self.zmqblockport = 28332 - self.zmqtxport = 28333 - self._suffix = None - self._ipv4 = None - self._exporter_name = None - # index of integers imported from graph file - # indicating which tanks to initially connect to - self.init_peers = [] - - def _parse_version(self, version): - if not version: - return - if version not in SUPPORTED_TAGS and not ("/" in version and "#" in version): - raise Exception( - f"Unsupported version: can't be generated from Docker images: {self.version}" - ) - self.version = version - - def parse_graph_node(self, node): - # Dynamically parse properties based on the schema - graph_properties = {} - for property, specs in self.warnet.graph_schema["node"]["properties"].items(): - value = node.get(property, specs.get("default")) - if property == "version": - self._parse_version(value) - setattr(self, property, value) - graph_properties[property] = value - - if self.version and self.image: - raise Exception( - f"Tank has {self.version=:} and {self.image=:} supplied and can't be built. Provide one or the other." - ) - - # Special handling for complex properties - if "ln" in node: - options = { - "impl": node["ln"], - "cb_image": node.get("ln_cb_image", None), - "ln_config": node.get("ln_config", ""), - } - from warnet.cln import CLNNode - from warnet.lnd import LNDNode - - if options["impl"] == "lnd": - options["ln_image"] = node.get("ln_image", "lightninglabs/lnd:v0.18.0-beta") - self.lnnode = LNDNode(self.warnet, self, self.warnet.container_interface, options) - elif options["impl"] == "cln": - options["ln_image"] = node.get("ln_image", "elementsproject/lightningd:v23.11") - self.lnnode = CLNNode(self.warnet, self, self.warnet.container_interface, options) - else: - raise Exception(f"Unsupported Lightning Network implementation: {options['impl']}") - - if "metrics" in node: - self.metrics = node["metrics"] - - logger.debug( - f"Parsed graph node: {self.index} with attributes: {[f'{key}={value}' for key, value in graph_properties.items()]}" - ) - - @classmethod - def from_graph_node(cls, index, warnet, tank=None): - assert index is not None - index = int(index) - self = tank - if self is None: - self = cls(index, warnet) - node = warnet.graph.nodes[index] - self.parse_graph_node(node) - return self - - @property - def suffix(self): - if self._suffix is None: - self._suffix = f"{self.index:06}" - return self._suffix - - @property - def ipv4(self): - if self._ipv4 is None: - self._ipv4 = generate_ipv4_addr(self.warnet.subnet) - return self._ipv4 - - @property - def exporter_name(self): - if self._exporter_name is None: - self._exporter_name = f"{self.network_name}-{CONTAINER_PREFIX_PROMETHEUS}-{self.suffix}" - return self._exporter_name - - @property - def status(self) -> RunningStatus: - return self.warnet.container_interface.get_status(self.index, ServiceType.BITCOIN) - - @exponential_backoff() - def exec(self, cmd: str): - return self.warnet.container_interface.exec_run(self.index, ServiceType.BITCOIN, cmd=cmd) - - def get_dns_addr(self) -> str: - dns_addr = self.warnet.container_interface.get_tank_dns_addr(self.index) - return dns_addr - - def get_ip_addr(self) -> str: - ip_addr = self.warnet.container_interface.get_tank_ip_addr(self.index) - return ip_addr - - def get_bitcoin_conf(self, nodes: list[str]) -> str: - conf = CONFIG_BASE - conf += f" -rpcuser={self.rpc_user}" - conf += f" -rpcpassword={self.rpc_password}" - conf += f" -rpcport={self.rpc_port}" - conf += f" -zmqpubrawblock=tcp://0.0.0.0:{self.zmqblockport}" - conf += f" -zmqpubrawtx=tcp://0.0.0.0:{self.zmqtxport}" - conf += " " + self.bitcoin_config - for node in nodes: - conf += f" -addnode={node}" - return conf - - def apply_network_conditions(self): - if self.netem is None: - return - - if not sanitize_tc_netem_command(self.netem): - logger.warning( - f"Not applying unsafe tc-netem conditions to tank {self.index}: `{self.netem}`" - ) - return - - # Apply the network condition to the container - try: - self.exec(self.netem) - logger.info( - f"Successfully applied network conditions to tank {self.index}: `{self.netem}`" - ) - except Exception as e: - logger.error( - f"Error applying network conditions to tank {self.index}: `{self.netem}` ({e})" - ) - - def export(self, config: object, tar_file): - if self.lnnode is not None: - self.lnnode.export(config, tar_file) diff --git a/src/warnet/test_framework_bridge.py b/src/warnet/test_framework_bridge.py deleted file mode 100644 index 7872284d2..000000000 --- a/src/warnet/test_framework_bridge.py +++ /dev/null @@ -1,406 +0,0 @@ -import argparse -import configparser -import ipaddress -import logging -import os -import pathlib -import random -import signal -import sys -import tempfile - -from test_framework.authproxy import AuthServiceProxy -from test_framework.p2p import NetworkThread -from test_framework.test_framework import ( - TMPDIR_PREFIX, - BitcoinTestFramework, - TestStatus, -) -from test_framework.test_node import TestNode -from test_framework.util import PortSeed, get_rpc_proxy - -from .warnet import Warnet - - -# Ensure that all RPC calls are made with brand new http connections -def auth_proxy_request(self, method, path, postdata): - self._set_conn() # creates new http client connection - return self.oldrequest(method, path, postdata) - - -AuthServiceProxy.oldrequest = AuthServiceProxy._request -AuthServiceProxy._request = auth_proxy_request - - -class WarnetTestFramework(BitcoinTestFramework): - def set_test_params(self): - pass - - def run_test(self): - pass - - def handle_sigterm(self, signum, frame): - print("SIGTERM received, stopping...") - self.shutdown() - sys.exit(0) - - # The following functions are chopped-up hacks of - # the original methods from BitcoinTestFramework - - def setup(self): - signal.signal(signal.SIGTERM, self.handle_sigterm) - - # Must setup warnet first to avoid double formatting - self.warnet = Warnet.from_network(self.options.network) - # hacked from _start_logging() - # Scenarios will log plain messages to stdout only, which will can redirected by warnet - self.log = logging.getLogger(self.__class__.__name__) - self.log.setLevel(logging.INFO) # set this to DEBUG to see ALL RPC CALLS - - # Because scenarios run in their own subprocess, the logger here - # is not the same as the warnet server or other global loggers. - # Scenarios log directly to stdout which gets picked up by the - # subprocess manager in the server, and reprinted to the global log. - ch = logging.StreamHandler(sys.stdout) - formatter = logging.Formatter(fmt="%(name)-8s %(message)s") - ch.setFormatter(formatter) - self.log.addHandler(ch) - - for i, tank in enumerate(self.warnet.tanks): - ip = tank.ipv4 - self.log.info(f"Adding TestNode {i} from tank {tank.index} with IP {ip}") - node = TestNode( - i, - pathlib.Path(), # datadir path - chain=tank.bitcoin_network, - rpchost=ip, - timewait=60, - timeout_factor=self.options.timeout_factor, - bitcoind=None, - bitcoin_cli=None, - cwd=self.options.tmpdir, - coverage_dir=self.options.coveragedir, - ) - node.rpc = get_rpc_proxy( - f"http://{tank.rpc_user}:{tank.rpc_password}@{ip}:{tank.rpc_port}", - i, - timeout=60, - coveragedir=self.options.coveragedir, - ) - node.rpc_connected = True - self.nodes.append(node) - - self.num_nodes = len(self.nodes) - - # Set up temp directory and start logging - if self.options.tmpdir: - self.options.tmpdir = os.path.abspath(self.options.tmpdir) - os.makedirs(self.options.tmpdir, exist_ok=False) - else: - self.options.tmpdir = tempfile.mkdtemp(prefix=TMPDIR_PREFIX) - - # self.options.cachedir = os.path.abspath(self.options.cachedir) - - # config = self.config - - # self.set_binary_paths() - - # os.environ['PATH'] = os.pathsep.join([ - # os.path.join(config['environment']['BUILDDIR'], 'src'), - # os.path.join(config['environment']['BUILDDIR'], 'src', 'qt'), os.environ['PATH'] - # ]) - - # Set up temp directory and start logging - # if self.options.tmpdir: - # self.options.tmpdir = os.path.abspath(self.options.tmpdir) - # os.makedirs(self.options.tmpdir, exist_ok=False) - # else: - # self.options.tmpdir = tempfile.mkdtemp(prefix=TMPDIR_PREFIX) - # self._start_logging() - - # Seed the PRNG. Note that test runs are reproducible if and only if - # a single thread accesses the PRNG. For more information, see - # https://docs.python.org/3/library/random.html#notes-on-reproducibility. - # The network thread shouldn't access random. If we need to change the - # network thread to access randomness, it should instantiate its own - # random.Random object. - seed = self.options.randomseed - - if seed is None: - seed = random.randrange(sys.maxsize) - else: - self.log.info(f"User supplied random seed {seed}") - - random.seed(seed) - self.log.info(f"PRNG seed is: {seed}") - - self.log.debug("Setting up network thread") - self.network_thread = NetworkThread() - self.network_thread.start() - - # if self.options.usecli: - # if not self.supports_cli: - # raise SkipTest("--usecli specified but test does not support using CLI") - # self.skip_if_no_cli() - # self.skip_test_if_missing_module() - # self.setup_chain() - # self.setup_network() - - self.success = TestStatus.PASSED - - def parse_args(self): - previous_releases_path = "" - parser = argparse.ArgumentParser(usage="%(prog)s [options]") - parser.add_argument( - "--nocleanup", - dest="nocleanup", - default=False, - action="store_true", - help="Leave bitcoinds and test.* datadir on exit or error", - ) - parser.add_argument( - "--nosandbox", - dest="nosandbox", - default=False, - action="store_true", - help="Don't use the syscall sandbox", - ) - parser.add_argument( - "--noshutdown", - dest="noshutdown", - default=False, - action="store_true", - help="Don't stop bitcoinds after the test execution", - ) - parser.add_argument( - "--cachedir", - dest="cachedir", - default=None, - help="Directory for caching pregenerated datadirs (default: %(default)s)", - ) - parser.add_argument( - "--tmpdir", dest="tmpdir", default=None, help="Root directory for datadirs" - ) - parser.add_argument( - "-l", - "--loglevel", - dest="loglevel", - default="DEBUG", - help="log events at this level and higher to the console. Can be set to DEBUG, INFO, WARNING, ERROR or CRITICAL. Passing --loglevel DEBUG will output all logs to console. Note that logs at all levels are always written to the test_framework.log file in the temporary test directory.", - ) - parser.add_argument( - "--tracerpc", - dest="trace_rpc", - default=False, - action="store_true", - help="Print out all RPC calls as they are made", - ) - parser.add_argument( - "--portseed", - dest="port_seed", - default=0, - help="The seed to use for assigning port numbers (default: current process id)", - ) - parser.add_argument( - "--previous-releases", - dest="prev_releases", - default=None, - action="store_true", - help="Force test of previous releases (default: %(default)s)", - ) - parser.add_argument( - "--coveragedir", - dest="coveragedir", - default=None, - help="Write tested RPC commands into this directory", - ) - parser.add_argument( - "--configfile", - dest="configfile", - default=None, - help="Location of the test framework config file (default: %(default)s)", - ) - parser.add_argument( - "--pdbonfailure", - dest="pdbonfailure", - default=False, - action="store_true", - help="Attach a python debugger if test fails", - ) - parser.add_argument( - "--usecli", - dest="usecli", - default=False, - action="store_true", - help="use bitcoin-cli instead of RPC for all commands", - ) - parser.add_argument( - "--perf", - dest="perf", - default=False, - action="store_true", - help="profile running nodes with perf for the duration of the test", - ) - parser.add_argument( - "--valgrind", - dest="valgrind", - default=False, - action="store_true", - help="run nodes under the valgrind memory error detector: expect at least a ~10x slowdown. valgrind 3.14 or later required.", - ) - parser.add_argument( - "--randomseed", - default=0x7761726E6574, # "warnet" ascii - help="set a random seed for deterministically reproducing a previous test run", - ) - parser.add_argument( - "--timeout-factor", - dest="timeout_factor", - default=1, - help="adjust test timeouts by a factor. Setting it to 0 disables all timeouts", - ) - parser.add_argument( - "--network", - dest="network", - default="warnet", - help="Designate which warnet this should run on (default: warnet)", - ) - parser.add_argument( - "--v2transport", - dest="v2transport", - default=False, - action="store_true", - help="use BIP324 v2 connections between all nodes by default", - ) - - self.add_options(parser) - # Running TestShell in a Jupyter notebook causes an additional -f argument - # To keep TestShell from failing with an "unrecognized argument" error, we add a dummy "-f" argument - # source: https://stackoverflow.com/questions/48796169/how-to-fix-ipykernel-launcher-py-error-unrecognized-arguments-in-jupyter/56349168#56349168 - parser.add_argument("-f", "--fff", help="a dummy argument to fool ipython", default="1") - self.options = parser.parse_args() - if self.options.timeout_factor == 0: - self.options.timeout_factor = 99999 - self.options.timeout_factor = self.options.timeout_factor or ( - 4 if self.options.valgrind else 1 - ) - self.options.previous_releases_path = previous_releases_path - config = configparser.ConfigParser() - if self.options.configfile is not None: - with open(self.options.configfile) as f: - config.read_file(f) - - config["environment"] = {"PACKAGE_BUGREPORT": ""} - - self.config = config - - if "descriptors" not in self.options: - # Wallet is not required by the test at all and the value of self.options.descriptors won't matter. - # It still needs to exist and be None in order for tests to work however. - # So set it to None to force -disablewallet, because the wallet is not needed. - self.options.descriptors = None - elif self.options.descriptors is None: - # Some wallet is either required or optionally used by the test. - # Prefer SQLite unless it isn't available - if self.is_sqlite_compiled(): - self.options.descriptors = True - elif self.is_bdb_compiled(): - self.options.descriptors = False - else: - # If neither are compiled, tests requiring a wallet will be skipped and the value of self.options.descriptors won't matter - # It still needs to exist and be None in order for tests to work however. - # So set it to None, which will also set -disablewallet. - self.options.descriptors = None - - PortSeed.n = self.options.port_seed - - def connect_nodes(self, a, b, *, peer_advertises_v2=None, wait_for_connect: bool = True): - """ - Kwargs: - wait_for_connect: if True, block until the nodes are verified as connected. You might - want to disable this when using -stopatheight with one of the connected nodes, - since there will be a race between the actual connection and performing - the assertions before one node shuts down. - """ - from_connection = self.nodes[a] - to_connection = self.nodes[b] - - to_ip_port = self.warnet.tanks[b].get_dns_addr() - from_ip_port = self.warnet.tanks[a].get_ip_addr() - - if peer_advertises_v2 is None: - peer_advertises_v2 = self.options.v2transport - - if peer_advertises_v2: - from_connection.addnode(node=to_ip_port, command="onetry", v2transport=True) - else: - # skip the optional third argument (default false) for - # compatibility with older clients - from_connection.addnode(to_ip_port, "onetry") - - if not wait_for_connect: - return - - def get_peer_ip(peer): - try: # we encounter a regular ip address - ip_addr = str(ipaddress.ip_address(peer["addr"].split(":")[0])) - return ip_addr - except ValueError as err: # or we encounter a service name - try: - # NETWORK-tank-TANK_INDEX-service - # NETWORK-test-TEST-tank-TANK_INDEX-service - tank_index = int(peer["addr"].split("-")[-2]) - except (ValueError, IndexError) as inner_err: - raise ValueError( - "could not derive tank index from service name: {} {}".format( - peer["addr"], inner_err - ) - ) from err - - ip_addr = self.warnet.tanks[tank_index].get_ip_addr() - return ip_addr - - # poll until version handshake complete to avoid race conditions - # with transaction relaying - # See comments in net_processing: - # * Must have a version message before anything else - # * Must have a verack message before anything else - self.wait_until( - lambda: any( - peer["addr"] == to_ip_port and peer["version"] != 0 - for peer in from_connection.getpeerinfo() - ) - ) - self.wait_until( - lambda: any( - get_peer_ip(peer) == from_ip_port and peer["version"] != 0 - for peer in to_connection.getpeerinfo() - ) - ) - self.wait_until( - lambda: any( - peer["addr"] == to_ip_port and peer["bytesrecv_per_msg"].pop("verack", 0) >= 21 - for peer in from_connection.getpeerinfo() - ) - ) - self.wait_until( - lambda: any( - get_peer_ip(peer) == from_ip_port - and peer["bytesrecv_per_msg"].pop("verack", 0) >= 21 - for peer in to_connection.getpeerinfo() - ) - ) - # The message bytes are counted before processing the message, so make - # sure it was fully processed by waiting for a ping. - self.wait_until( - lambda: any( - peer["addr"] == to_ip_port and peer["bytesrecv_per_msg"].pop("pong", 0) >= 29 - for peer in from_connection.getpeerinfo() - ) - ) - self.wait_until( - lambda: any( - get_peer_ip(peer) == from_ip_port and peer["bytesrecv_per_msg"].pop("pong", 0) >= 29 - for peer in to_connection.getpeerinfo() - ) - ) diff --git a/src/warnet/users.py b/src/warnet/users.py new file mode 100644 index 000000000..24ddd9ff2 --- /dev/null +++ b/src/warnet/users.py @@ -0,0 +1,139 @@ +import difflib +import json +import os +import sys + +import click + +from warnet.constants import KUBECONFIG, KUBECONFIG_UNDO +from warnet.k8s import K8sError, open_kubeconfig, write_kubeconfig + + +@click.command() +@click.option("--revert", is_flag=True, default=False, show_default=True) +@click.argument("auth_config", type=str, required=False) +def auth(revert, auth_config): + """Authenticate with a Warnet cluster using a kubernetes config file""" + if revert: + auth_config = KUBECONFIG_UNDO + elif not auth_config: + raise click.UsageError("Missing argument: AUTH_CONFIG") + + try: + auth_config = open_kubeconfig(auth_config) + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not open auth_config: {auth_config}", fg="red") + sys.exit(1) + + is_first_config = False + if not os.path.exists(KUBECONFIG): + os.makedirs(os.path.dirname(KUBECONFIG), exist_ok=True) + try: + write_kubeconfig(auth_config, KUBECONFIG) + is_first_config = True + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not write KUBECONFIG: {KUBECONFIG}", fg="red") + sys.exit(1) + + try: + base_config = open_kubeconfig(KUBECONFIG) + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not open KUBECONFIG: {KUBECONFIG}", fg="red") + sys.exit(1) + + try: + write_kubeconfig(base_config, KUBECONFIG_UNDO) + click.secho(f"Backed up current kubeconfig to: {KUBECONFIG_UNDO}", fg="green") + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not backup current kubeconfig to {KUBECONFIG_UNDO}", fg="red") + + if not is_first_config: + for category in ["clusters", "users", "contexts"]: + if category in auth_config: + if category in base_config and not base_config[category]: + base_config[category] = [] + merge_entries(category, base_config, auth_config) + + new_current_context = auth_config.get("current-context") + base_config["current-context"] = new_current_context + + # Check if the new current context has an explicit namespace + context_entry = next( + (ctx for ctx in base_config["contexts"] if ctx["name"] == new_current_context), None + ) + if context_entry and "namespace" not in context_entry["context"]: + click.secho( + f"Warning: The context '{new_current_context}' does not have an explicit namespace.", + fg="yellow", + ) + + try: + write_kubeconfig(base_config, KUBECONFIG) + click.secho(f"Updated kubeconfig with authorization data: {KUBECONFIG}", fg="green") + except K8sError as e: + click.secho(e, fg="yellow") + click.secho(f"Could not write KUBECONFIG: {KUBECONFIG}", fg="red") + sys.exit(1) + + try: + base_config = open_kubeconfig(KUBECONFIG) + click.secho( + f"Warnet's current context is now set to: {base_config['current-context']}", fg="green" + ) + except K8sError as e: + click.secho(f"Error reading from {KUBECONFIG}: {e}", fg="red") + sys.exit(1) + + +def merge_entries(category, base_config, auth_config): + name = "name" + base_list = base_config.setdefault(category, []) + auth_list = auth_config[category] + base_entry_names = {entry[name] for entry in base_list} # Extract existing names + for auth_entry in auth_list: + if auth_entry[name] in base_entry_names: + existing_entry = next( + base_entry for base_entry in base_list if base_entry[name] == auth_entry[name] + ) + if existing_entry != auth_entry: + # Show diff between existing and new entry + existing_entry_str = json.dumps(existing_entry, indent=2, sort_keys=True) + auth_entry_str = json.dumps(auth_entry, indent=2, sort_keys=True) + diff = difflib.unified_diff( + existing_entry_str.splitlines(), + auth_entry_str.splitlines(), + fromfile="Existing Entry", + tofile="New Entry", + lineterm="", + ) + click.echo("Differences between existing and new entry:\n") + click.echo("\n".join(diff)) + + if click.confirm( + f"The '{category}' section key '{auth_entry[name]}' already exists and differs. Overwrite?", + default=False, + ): + # Find and replace the existing entry + base_list[:] = [ + base_entry if base_entry[name] != auth_entry[name] else auth_entry + for base_entry in base_list + ] + click.secho( + f"Overwrote '{category}' section key '{auth_entry[name]}'", fg="yellow" + ) + else: + click.secho( + f"Skipped '{category}' section key '{auth_entry[name]}'", fg="yellow" + ) + else: + click.secho( + f"Entry for '{category}' section key '{auth_entry[name]}' is identical. No changes made.", + fg="blue", + ) + else: + base_list.append(auth_entry) + click.secho(f"Added new '{category}' section key '{auth_entry[name]}'", fg="green") diff --git a/src/warnet/util.py b/src/warnet/util.py new file mode 100644 index 000000000..90cc7b91d --- /dev/null +++ b/src/warnet/util.py @@ -0,0 +1,65 @@ +def create_cycle_graph(n: int, version: str, bitcoin_conf: str | None, random_version: bool): + raise NotImplementedError("create_cycle_graph function is not implemented") + + +def parse_bitcoin_conf(file_content): + """ + Custom parser for INI-style bitcoin.conf + + Args: + - file_content (str): The content of the INI-style file. + + Returns: + - dict: A dictionary representation of the file content. + Key-value pairs are stored as tuples so one key may have + multiple values. Sections are represented as arrays of these tuples. + """ + current_section = None + result = {current_section: []} + + for line in file_content.splitlines(): + line = line.strip() + if not line or line.startswith("#"): + continue + + if line.startswith("[") and line.endswith("]"): + current_section = line[1:-1] + result[current_section] = [] + elif "=" in line: + key, value = line.split("=", 1) + result[current_section].append((key.strip(), value.strip())) + + return result + + +def dump_bitcoin_conf(conf_dict, for_graph=False): + """ + Converts a dictionary representation of bitcoin.conf content back to INI-style string. + + Args: + - conf_dict (dict): A dictionary representation of the file content. + + Returns: + - str: The INI-style string representation of the input dictionary. + """ + result = [] + + # Print global section at the top first + values = conf_dict[None] + for sub_key, sub_value in values: + result.append(f"{sub_key}={sub_value}") + + # Then print any named subsections + for section, values in conf_dict.items(): + if section is not None: + result.append(f"\n[{section}]") + else: + continue + for sub_key, sub_value in values: + result.append(f"{sub_key}={sub_value}") + + if for_graph: + return ",".join(result) + + # Terminate file with newline + return "\n".join(result) + "\n" diff --git a/src/warnet/utils.py b/src/warnet/utils.py deleted file mode 100644 index 23dad566b..000000000 --- a/src/warnet/utils.py +++ /dev/null @@ -1,479 +0,0 @@ -import functools -import ipaddress -import json -import logging -import os -import random -import re -import stat -import subprocess -import sys -import time -from io import BytesIO -from pathlib import Path - -import networkx as nx -from jsonschema import validate -from test_framework.messages import ser_uint256 -from test_framework.p2p import MESSAGEMAP -from warnet import SRC_DIR - -logger = logging.getLogger("utils") - - -SUPPORTED_TAGS = ["27.0", "26.0", "25.1", "24.2", "23.2", "22.2"] -DEFAULT_TAG = SUPPORTED_TAGS[0] -WEIGHTED_TAGS = [ - tag for index, tag in enumerate(reversed(SUPPORTED_TAGS)) for _ in range(index + 1) -] - - -class NonErrorFilter(logging.Filter): - def filter(self, record: logging.LogRecord) -> bool | logging.LogRecord: - return record.levelno <= logging.INFO - - -def exponential_backoff(max_retries=5, base_delay=1, max_delay=32): - """ - A decorator for exponential backoff. - - Parameters: - - max_retries: Maximum number of retries before giving up. - - base_delay: Initial delay in seconds. - - max_delay: Maximum delay in seconds. - """ - - def decorator(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - retries = 0 - while retries < max_retries: - try: - return func(*args, **kwargs) - except Exception as e: - error_msg = str(e).replace("\n", " ").replace("\t", " ") - logger.error(f"rpc error: {error_msg}") - retries += 1 - if retries == max_retries: - raise e - delay = min(base_delay * (2**retries), max_delay) - logger.warning(f"exponential_backoff: retry in {delay} seconds...") - time.sleep(delay) - - return wrapper - - return decorator - - -def handle_json(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - result = "" - try: - result = func(*args, **kwargs) - logger.debug(f"{result=:}") - if isinstance(result, dict): - return result - parsed_result = json.loads(result) - return parsed_result - except json.JSONDecodeError as e: - logging.error( - f"JSON parsing error in {func.__name__}: {e}. Undecodable result: {result}" - ) - raise - except Exception as e: - logger.error(f"Error in {func.__name__}: {e}") - raise - - return wrapper - - -def get_architecture(): - """ - Get the architecture of the machine. - :return: The architecture of the machine or None if an error occurred - """ - result = subprocess.run(["uname", "-m"], stdout=subprocess.PIPE) - arch = result.stdout.decode("utf-8").strip() - if arch == "x86_64": - arch = "amd64" - if arch is None: - raise Exception("Failed to detect architecture.") - return arch - - -def generate_ipv4_addr(subnet): - """ - Generate a valid random IPv4 address within the given subnet. - - :param subnet: Subnet in CIDR notation (e.g., '100.0.0.0/8') - :return: Random IP address within the subnet - """ - reserved_ips = [ - "0.0.0.0/8", - "10.0.0.0/8", - "100.64.0.0/10", - "127.0.0.0/8", - "169.254.0.0/16", - "172.16.0.0/12", - "192.0.0.0/24", - "192.0.2.0/24", - "192.88.99.0/24", - "192.168.0.0/16", - "198.18.0.0/15", - "198.51.100.0/24", - "203.0.113.0/24", - "224.0.0.0/4", - ] - - def is_public(ip): - for reserved in reserved_ips: - if ipaddress.ip_address(ip) in ipaddress.ip_network(reserved, strict=False): - return False - return True - - network = ipaddress.ip_network(subnet, strict=False) - - # Generate a random IP within the subnet range - while True: - ip_int = random.randint(int(network.network_address), int(network.broadcast_address)) - ip_str = str(ipaddress.ip_address(ip_int)) - if is_public(ip_str): - return ip_str - - -def sanitize_tc_netem_command(command: str) -> bool: - """ - Sanitize the tc-netem command to ensure it's valid and safe to execute, as we run it as root on a container. - - Args: - - command (str): The tc-netem command to sanitize. - - Returns: - - bool: True if the command is valid and safe, False otherwise. - """ - if not command.startswith("tc qdisc add dev eth0 root netem"): - return False - - tokens = command.split()[7:] # Skip the prefix - - # Valid tc-netem parameters and their patterns - valid_params = { - "delay": r"^\d+ms(\s\d+ms)?(\sdistribution\s(normal|pareto|paretonormal|uniform))?$", - "loss": r"^\d+(\.\d+)?%$", - "duplicate": r"^\d+(\.\d+)?%$", - "corrupt": r"^\d+(\.\d+)?%$", - "reorder": r"^\d+(\.\d+)?%\s\d+(\.\d+)?%$", - "rate": r"^\d+(kbit|mbit|gbit)$", - } - - # Validate each param - i = 0 - while i < len(tokens): - param = tokens[i] - if param not in valid_params: - return False - i += 1 - value_tokens = [] - while i < len(tokens) and tokens[i] not in valid_params: - value_tokens.append(tokens[i]) - i += 1 - value = " ".join(value_tokens) - if not re.match(valid_params[param], value): - return False - - return True - - -def parse_bitcoin_conf(file_content): - """ - Custom parser for INI-style bitcoin.conf - - Args: - - file_content (str): The content of the INI-style file. - - Returns: - - dict: A dictionary representation of the file content. - Key-value pairs are stored as tuples so one key may have - multiple values. Sections are represented as arrays of these tuples. - """ - current_section = None - result = {current_section: []} - - for line in file_content.splitlines(): - line = line.strip() - if not line or line.startswith("#"): - continue - - if line.startswith("[") and line.endswith("]"): - current_section = line[1:-1] - result[current_section] = [] - elif "=" in line: - key, value = line.split("=", 1) - result[current_section].append((key.strip(), value.strip())) - - return result - - -def dump_bitcoin_conf(conf_dict, for_graph=False): - """ - Converts a dictionary representation of bitcoin.conf content back to INI-style string. - - Args: - - conf_dict (dict): A dictionary representation of the file content. - - Returns: - - str: The INI-style string representation of the input dictionary. - """ - result = [] - - # Print global section at the top first - values = conf_dict[None] - for sub_key, sub_value in values: - result.append(f"{sub_key}={sub_value}") - - # Then print any named subsections - for section, values in conf_dict.items(): - if section is not None: - result.append(f"\n[{section}]") - else: - continue - for sub_key, sub_value in values: - result.append(f"{sub_key}={sub_value}") - - if for_graph: - return ",".join(result) - - # Terminate file with newline - return "\n".join(result) + "\n" - - -def to_jsonable(obj): - HASH_INTS = [ - "blockhash", - "block_hash", - "hash", - "hashMerkleRoot", - "hashPrevBlock", - "hashstop", - "prev_header", - "sha256", - "stop_hash", - ] - - HASH_INT_VECTORS = [ - "hashes", - "headers", - "vHave", - "vHash", - ] - - if hasattr(obj, "__dict__"): - return obj.__dict__ - elif hasattr(obj, "__slots__"): - ret = {} # type: Any - for slot in obj.__slots__: - val = getattr(obj, slot, None) - if slot in HASH_INTS and isinstance(val, int): - ret[slot] = ser_uint256(val).hex() - elif slot in HASH_INT_VECTORS and all(isinstance(a, int) for a in val): - ret[slot] = [ser_uint256(a).hex() for a in val] - else: - ret[slot] = to_jsonable(val) - return ret - elif isinstance(obj, list): - return [to_jsonable(a) for a in obj] - elif isinstance(obj, bytes): - return obj.hex() - else: - return obj - - -# This function is a hacked-up copy of process_file() from -# Bitcoin Core contrib/message-capture/message-capture-parser.py -def parse_raw_messages(blob, outbound): - TIME_SIZE = 8 - LENGTH_SIZE = 4 - MSGTYPE_SIZE = 12 - - messages = [] - offset = 0 - while True: - # Read the Header - header_len = TIME_SIZE + LENGTH_SIZE + MSGTYPE_SIZE - tmp_header_raw = blob[offset : offset + header_len] - - offset = offset + header_len - if not tmp_header_raw: - break - tmp_header = BytesIO(tmp_header_raw) - time = int.from_bytes(tmp_header.read(TIME_SIZE), "little") # type: int - msgtype = tmp_header.read(MSGTYPE_SIZE).split(b"\x00", 1)[0] # type: bytes - length = int.from_bytes(tmp_header.read(LENGTH_SIZE), "little") # type: int - - # Start converting the message to a dictionary - msg_dict = {} - msg_dict["outbound"] = outbound - msg_dict["time"] = time - msg_dict["size"] = length # "size" is less readable here, but more readable in the output - - msg_ser = BytesIO(blob[offset : offset + length]) - offset = offset + length - - # Determine message type - if msgtype not in MESSAGEMAP: - # Unrecognized message type - try: - msgtype_tmp = msgtype.decode() - if not msgtype_tmp.isprintable(): - raise UnicodeDecodeError - msg_dict["msgtype"] = msgtype_tmp - except UnicodeDecodeError: - msg_dict["msgtype"] = "UNREADABLE" - msg_dict["body"] = msg_ser.read().hex() - msg_dict["error"] = "Unrecognized message type." - messages.append(msg_dict) - print(f"WARNING - Unrecognized message type {msgtype}", file=sys.stderr) - continue - - # Deserialize the message - msg = MESSAGEMAP[msgtype]() - msg_dict["msgtype"] = msgtype.decode() - - try: - msg.deserialize(msg_ser) - except KeyboardInterrupt: - raise - except Exception: - # Unable to deserialize message body - msg_ser.seek(0, os.SEEK_SET) - msg_dict["body"] = msg_ser.read().hex() - msg_dict["error"] = "Unable to deserialize message." - messages.append(msg_dict) - print("WARNING - Unable to deserialize message", file=sys.stderr) - continue - - # Convert body of message into a jsonable object - if length: - msg_dict["body"] = to_jsonable(msg) - messages.append(msg_dict) - return messages - - -def gen_config_dir(network: str) -> Path: - """ - Determine a config dir based on network name - """ - config_dir = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.warnet")) - config_dir = Path(config_dir) / "warnet" / network - return config_dir - - -def remove_version_prefix(version_str): - if version_str.startswith("0."): - return version_str[2:] - return version_str - - -def set_execute_permission(file_path): - current_permissions = os.stat(file_path).st_mode - os.chmod(file_path, current_permissions | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - - -def create_cycle_graph(n: int, version: str, bitcoin_conf: str | None, random_version: bool): - try: - # Use nx.MultiDiGraph() so we get directed edges (source->target) - # and still allow parallel edges (L1 p2p connections + LN channels) - graph = nx.generators.cycle_graph(n, nx.MultiDiGraph()) - except TypeError as e: - msg = f"Failed to create graph: {e}" - logger.error(msg) - return msg - - # Graph is a simply cycle graph with all nodes connected in a loop, including both ends. - # Ensure each node has at least 8 outbound connections by making 7 more outbound connections - for src_node in graph.nodes(): - logger.debug(f"Creating additional connections for node {src_node}") - for _ in range(8): - # Choose a random node to connect to - # Make sure it's not the same node and they aren't already connected in either direction - potential_nodes = [ - dst_node - for dst_node in range(n) - if dst_node != src_node - and not graph.has_edge(dst_node, src_node) - and not graph.has_edge(src_node, dst_node) - ] - if potential_nodes: - chosen_node = random.choice(potential_nodes) - graph.add_edge(src_node, chosen_node) - logger.debug(f"Added edge: {src_node}:{chosen_node}") - logger.debug(f"Node {src_node} edges: {graph.edges(src_node)}") - - # parse and process conf file - conf_contents = "" - if bitcoin_conf is not None: - conf = Path(bitcoin_conf) - if conf.is_file(): - with open(conf) as f: - # parse INI style conf then dump using for_graph - conf_dict = parse_bitcoin_conf(f.read()) - conf_contents = dump_bitcoin_conf(conf_dict, for_graph=True) - - # populate our custom fields - for i, node in enumerate(graph.nodes()): - if random_version: - graph.nodes[node]["version"] = random.choice(WEIGHTED_TAGS) - else: - # One node demoing the image tag - if i == 1: - graph.nodes[node]["image"] = f"bitcoindevproject/bitcoin:{version}" - else: - graph.nodes[node]["version"] = version - graph.nodes[node]["bitcoin_config"] = conf_contents - graph.nodes[node]["tc_netem"] = "" - graph.nodes[node]["build_args"] = "" - graph.nodes[node]["exporter"] = False - graph.nodes[node]["collect_logs"] = False - - convert_unsupported_attributes(graph) - return graph - - -def convert_unsupported_attributes(graph: nx.Graph): - # Sometimes networkx complains about invalid types when writing the graph - # (it just generated itself!). Try to convert them here just in case. - for _, node_data in graph.nodes(data=True): - for key, value in node_data.items(): - if isinstance(value, set): - node_data[key] = list(value) - elif isinstance(value, int | float | str): - continue - else: - node_data[key] = str(value) - - for _, _, edge_data in graph.edges(data=True): - for key, value in edge_data.items(): - if isinstance(value, set): - edge_data[key] = list(value) - elif isinstance(value, int | float | str): - continue - else: - edge_data[key] = str(value) - - -def load_schema(): - with open(SRC_DIR / "graph_schema.json") as schema_file: - return json.load(schema_file) - - -def validate_graph_schema(graph: nx.Graph): - """ - Validate a networkx.Graph against the node schema - """ - graph_schema = load_schema() - validate(instance=graph.graph, schema=graph_schema["graph"]) - for n in list(graph.nodes): - validate(instance=graph.nodes[n], schema=graph_schema["node"]) - for e in list(graph.edges): - validate(instance=graph.edges[e], schema=graph_schema["edge"]) diff --git a/src/warnet/warnet.py b/src/warnet/warnet.py deleted file mode 100644 index 67cd52002..000000000 --- a/src/warnet/warnet.py +++ /dev/null @@ -1,283 +0,0 @@ -""" -Warnet is the top-level class for a simulated network. -""" - -import base64 -import json -import logging -from pathlib import Path - -import networkx - -from .backend.kubernetes_backend import KubernetesBackend -from .tank import Tank -from .utils import gen_config_dir, load_schema, validate_graph_schema - -logger = logging.getLogger("warnet") - - -class Warnet: - def __init__(self, config_dir, network_name: str): - self.config_dir: Path = config_dir - self.config_dir.mkdir(parents=True, exist_ok=True) - self.container_interface = KubernetesBackend(config_dir, network_name) - self.bitcoin_network: str = "regtest" - self.network_name: str = "warnet" - self.subnet: str = "100.0.0.0/8" - self.graph: networkx.Graph | None = None - self.graph_name = "graph.graphml" - self.tanks: list[Tank] = [] - self.deployment_file: Path | None = None - self.graph_schema = load_schema() - self.services = [] - - def _warnet_dict_representation(self) -> dict: - repr = {} - # Warnet - repr["warnet_headers"] = [ - "Temp dir", - "Bitcoin network", - "Docker network", - "Subnet", - "Graph", - ] - repr["warnet"] = [ - [ - str(self.config_dir), - self.bitcoin_network, - self.network_name, - self.subnet, - str(self.graph), - ] - ] - - # Tanks - tank_headers = [ - "Index", - "Version", - "IPv4", - "bitcoin conf", - "tc_netem", - "LN", - "LN Image", - "LN IPv4", - ] - has_ln = any(tank.lnnode and tank.lnnode.impl for tank in self.tanks) - tanks = [] - for tank in self.tanks: - tank_data = [ - tank.index, - tank.version if tank.version else tank.image, - tank.ipv4, - tank.bitcoin_config, - tank.netem, - ] - if has_ln: - tank_data.extend( - [ - tank.lnnode.impl if tank.lnnode else "", - tank.lnnode.image if tank.lnnode else "", - tank.lnnode.ipv4 if tank.lnnode else "", - ] - ) - tanks.append(tank_data) - if not has_ln: - tank_headers.remove("LN") - tank_headers.remove("LN IPv4") - - repr["tank_headers"] = tank_headers - repr["tanks"] = tanks - - return repr - - @classmethod - def from_graph_file( - cls, - base64_graph: str, - config_dir: Path, - network: str = "warnet", - ): - self = cls(config_dir, network) - destination = self.config_dir / self.graph_name - destination.parent.mkdir(parents=True, exist_ok=True) - graph_file = base64.b64decode(base64_graph) - with open(destination, "wb") as f: - f.write(graph_file) - self.network_name = network - self.graph = networkx.parse_graphml( - graph_file.decode("utf-8"), node_type=int, force_multigraph=True - ) - validate_graph_schema(self.graph) - self.tanks_from_graph() - if "services" in self.graph.graph: - self.services = self.graph.graph["services"].split() - logger.info(f"Created Warnet using directory {self.config_dir}") - return self - - @classmethod - def from_graph(cls, graph, network="warnet"): - self = cls(Path(), network) - self.graph = graph - validate_graph_schema(self.graph) - self.tanks_from_graph() - if "services" in self.graph.graph: - self.services = self.graph.graph["services"].split() - logger.info(f"Created Warnet using directory {self.config_dir}") - return self - - @classmethod - def from_network(cls, network_name): - config_dir = gen_config_dir(network_name) - self = cls(config_dir, network_name) - self.network_name = network_name - # Get network graph edges from graph file (required for network restarts) - self.graph = networkx.read_graphml( - Path(self.config_dir / self.graph_name), node_type=int, force_multigraph=True - ) - validate_graph_schema(self.graph) - self.tanks_from_graph() - if "services" in self.graph.graph: - self.services = self.graph.graph["services"].split() - for tank in self.tanks: - tank._ipv4 = self.container_interface.get_tank_ipv4(tank.index) - return self - - def tanks_from_graph(self): - if not self.graph: - return - for node_id in self.graph.nodes(): - if int(node_id) != len(self.tanks): - raise Exception( - f"Node ID in graph must be incrementing integers (got '{node_id}', expected '{len(self.tanks)}')" - ) - tank = Tank.from_graph_node(node_id, self) - # import edges as list of destinations to connect to - for edge in self.graph.edges(data=True): - (src, dst, data) = edge - # Ignore LN edges for now - if "channel_open" in data: - continue - if src == node_id: - tank.init_peers.append(int(dst)) - self.tanks.append(tank) - logger.info(f"Imported {len(self.tanks)} tanks from graph") - - def apply_network_conditions(self): - for tank in self.tanks: - tank.apply_network_conditions() - - def warnet_build(self): - self.container_interface.build() - - def get_ln_node_from_tank(self, index): - return self.tanks[index].lnnode - - def warnet_up(self): - self.container_interface.up(self) - - def warnet_down(self): - self.container_interface.down(self) - - def generate_deployment(self): - self.container_interface.generate_deployment_file(self) - - # if "forkobserver" in self.services: - # self.write_fork_observer_config() - # if "addrmanobserver" in self.services: - # self.write_addrman_observer_config() - # if "grafana" in self.services: - # self.write_grafana_config() - # if "prometheus" in self.services: - # self.write_prometheus_config() - - # def write_fork_observer_config(self): - # src = FO_CONF_NAME - # dst = self.config_dir / FO_CONF_NAME - # shutil.copy(src, dst) - # with open(dst, "a") as f: - # for tank in self.tanks: - # f.write( - # f""" - # [[networks.nodes]] - # id = {tank.index} - # name = "Node {tank.index}" - # description = "Warnet tank {tank.index}" - # rpc_host = "{tank.ipv4}" - # rpc_port = {tank.rpc_port} - # rpc_user = "{tank.rpc_user}" - # rpc_password = "{tank.rpc_password}" - # """ - # ) - # logger.info(f"Wrote file: {dst}") - - # def write_addrman_observer_config(self): - # src = AO_CONF_NAME - # dst = self.config_dir / AO_CONF_NAME - # shutil.copy(src, dst) - # with open(dst, "a") as f: - # for tank in self.tanks: - # f.write( - # f""" - # [[nodes]] - # id = {tank.index} - # name = "node-{tank.index}" - # rpc_host = "{tank.ipv4}" - # rpc_port = {tank.rpc_port} - # rpc_user = "{tank.rpc_user}" - # rpc_password = "{tank.rpc_password}" - # """ - # ) - # logger.info(f"Wrote file: {dst}") - - # def write_grafana_config(self): - # src = GRAFANA_PROVISIONING - # dst = self.config_dir / GRAFANA_PROVISIONING - # shutil.copytree(src, dst, dirs_exist_ok=True) - # logger.info(f"Wrote directory: {dst}") - - # def write_prometheus_config(self): - # scrape_configs = [ - # { - # "job_name": "cadvisor", - # "scrape_interval": "15s", - # "static_configs": [{"targets": [f"{self.network_name}_cadvisor:8080"]}], - # } - # ] - # for tank in self.tanks: - # if tank.exporter: - # scrape_configs.append( - # { - # "job_name": tank.exporter_name, - # "scrape_interval": "5s", - # "static_configs": [{"targets": [f"{tank.exporter_name}:9332"]}], - # } - # ) - # config = {"global": {"scrape_interval": "15s"}, "scrape_configs": scrape_configs} - # prometheus_path = self.config_dir / PROM_CONF_NAME - # try: - # with open(prometheus_path, "w") as file: - # yaml.dump(config, file) - # logger.info(f"Wrote file: {prometheus_path}") - # except Exception as e: - # logger.error(f"An error occurred while writing to {prometheus_path}: {e}") - - def export(self, config: object, tar_file, exclude: list[int]): - for tank in self.tanks: - if tank.index not in exclude: - tank.export(config, tar_file) - - def wait_for_health(self): - self.container_interface.wait_for_healthy_tanks(self) - - def network_connected(self): - for tank in self.tanks: - peerinfo = json.loads(self.container_interface.get_bitcoin_cli(tank, "getpeerinfo")) - manuals = 0 - for peer in peerinfo: - if peer["connection_type"] == "manual": - manuals += 1 - # Even if more edges are specifed, bitcoind only allows - # 8 manual outbound connections - if min(8, len(tank.init_peers)) > manuals: - return False - return True diff --git a/test/bitcoin_rpc_args_test.py b/test/bitcoin_rpc_args_test.py new file mode 100755 index 000000000..4015c4e13 --- /dev/null +++ b/test/bitcoin_rpc_args_test.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 + +import shlex +import sys +from pathlib import Path +from unittest.mock import patch + +# Import TestBase for consistent test structure +from test_base import TestBase + +from warnet.bitcoin import _rpc + +# Import _rpc from warnet.bitcoin and run_command from warnet.process +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +# Edge cases to test +EDGE_CASES = [ + # (params, expected_cmd_suffix, should_fail) + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]'], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]'], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "economical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "economical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "'economical'"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "'economical'"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", '"economical"'], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", '"economical"'], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco nomical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco'nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco'nomical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", 'eco"nomical'], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", 'eco"nomical'], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco$nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco$nomical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco;nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco;nomical"], + False, + ), + ( + ['[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco|nomical"], + ["send", '[{"bcrt1qsrsmr7f77kcxggk99yp2h8yjzv29lxhet4efwn":0.1}]', "1", "eco|nomical"], + False, + ), + # Malformed JSON (should fail gracefully) + ( + [ + '[{"desc":"wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/0/*)#5j6mshps","timestamp":0,"active":true,"internal":false,"range":[0,999],"next":0,"next_index":0}' + ], # Missing closing bracket + [ + "importdescriptors", + '[{"desc":"wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/0/*)#5j6mshps","timestamp":0,"active":true,"internal":false,"range":[0,999],"next":0,"next_index":0}', + ], + True, # Should fail due to malformed JSON + ), + # Unicode in descriptors + ( + [ + '[{"desc":"wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/0/*)#5j6mshps","timestamp":0,"active":true,"internal":false,"range":[0,999],"next":0,"next_index":0,"label":"测试"}' + ], + [ + "importdescriptors", + '[{"desc":"wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/0/*)#5j6mshps","timestamp":0,"active":true,"internal":false,"range":[0,999],"next":0,"next_index":0,"label":"测试"}', + ], + False, + ), + # Long descriptor (simulate, should not crash, may fail) + ( + [ + "[{'desc':'wpkh([d34db33f/84h/0h/0h/0/0]xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKp...','range':[0,1000]}]" + ], + [ + "send", + "[{'desc':'wpkh([d34db33f/84h/0h/0h/0/0]xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKp...','range':[0,1000]}]", + ], + False, # Updated to False since it now works correctly + ), + # Empty params + ([], ["send"], False), +] + + +class BitcoinRPCRPCArgsTest(TestBase): + def __init__(self): + super().__init__() + self.tank = "tank-0027" + self.namespace = "default" + self.captured_cmds = [] + + def run_test(self): + self.log.info("Testing bitcoin _rpc argument handling edge cases") + for params, expected_suffix, should_fail in EDGE_CASES: + # Extract the method from the expected suffix + method = expected_suffix[0] + + with patch("warnet.bitcoin.run_command") as mock_run_command: + mock_run_command.return_value = "MOCKED" + try: + _rpc(self.tank, method, params, self.namespace) + called_args = mock_run_command.call_args[0][0] + self.captured_cmds.append(called_args) + # Parse the command string into arguments for comparison + parsed_args = shlex.split(called_args) + assert parsed_args[-len(expected_suffix) :] == expected_suffix, ( + f"Params: {params} | Got: {parsed_args[-len(expected_suffix) :]} | Expected: {expected_suffix}" + ) + if should_fail: + self.log.info(f"Expected failure for params: {params}, but succeeded.") + except Exception as e: + if not should_fail: + raise AssertionError(f"Unexpected failure for params: {params}: {e}") from e + self.log.info(f"Expected failure for params: {params}: {e}") + self.log.info("All edge case argument tests passed.") + + +if __name__ == "__main__": + test = BitcoinRPCRPCArgsTest() + test.run_test() diff --git a/test/build_branch_test.py b/test/build_branch_test.py deleted file mode 100755 index bbce564ce..000000000 --- a/test/build_branch_test.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 - -import json -import os -from pathlib import Path - -from test_base import TestBase - - -class BuildBranchTest(TestBase): - def __init__(self): - super().__init__() - self.graph_file_path = Path(os.path.dirname(__file__)) / "data" / "build_v24_test.graphml" - - def run_test(self): - self.start_server() - try: - self.setup_network() - self.wait_for_p2p_connections() - self.check_build_flags() - finally: - self.stop_server() - - def setup_network(self): - self.log.info("Setting up network") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) - self.wait_for_all_tanks_status(target="running", timeout=10 * 60) - self.wait_for_all_edges() - - def wait_for_p2p_connections(self): - self.log.info("Waiting for P2P connections") - self.wait_for_predicate(self.check_peers, timeout=5 * 60) - - def check_peers(self): - info0 = json.loads(self.warcli("bitcoin rpc 0 getpeerinfo")) - info1 = json.loads(self.warcli("bitcoin rpc 1 getpeerinfo")) - self.log.debug( - f"Waiting for both nodes to get one peer: node0: {len(info0)}, node1: {len(info1)}" - ) - return len(info0) == 1 and len(info1) == 1 - - def check_build_flags(self): - self.log.info("Checking build flags") - release_help = self.get_tank(0).exec("bitcoind -h") - build_help = self.get_tank(1).exec("bitcoind -h") - - assert "zmqpubhashblock" in release_help, "zmqpubhashblock not found in release help" - assert ( - "zmqpubhashblock" not in build_help - ), "zmqpubhashblock found in build help, but it shouldn't be" - - self.log.info("Build flags check passed") - - -if __name__ == "__main__": - test = BuildBranchTest() - test.run_test() diff --git a/test/conf_test.py b/test/conf_test.py new file mode 100755 index 000000000..fc3c8f4b9 --- /dev/null +++ b/test/conf_test.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import json +import os +import re +from pathlib import Path + +from test_base import TestBase + +from warnet.control import stop_scenario +from warnet.k8s import get_mission +from warnet.status import _get_deployed_scenarios as scenarios_deployed + + +class ConfTest(TestBase): + def __init__(self): + super().__init__() + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "bitcoin_conf" + self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" + + def run_test(self): + try: + self.setup_network() + self.check_uacomment() + self.check_single_miner() + finally: + self.cleanup() + + def setup_network(self): + self.log.info("Setting up network") + self.log.info(self.warnet(f"deploy {self.network_dir}")) + self.wait_for_all_tanks_status(target="running") + + def check_uacomment(self): + tanks = get_mission("tank") + + def get_uacomment(): + for tank in tanks[::-1]: + try: + name = tank.metadata.name + info = json.loads(self.warnet(f"bitcoin rpc {name} getnetworkinfo")) + subver = info["subversion"] + + # Regex pattern to match the uacomment inside parentheses + # e.g. /Satoshi:27.0.0(tank-0027)/ + pattern = r"\(([^)]+)\)" + match = re.search(pattern, subver) + if match: + uacomment = match.group(1) + assert uacomment == name + else: + return False + except Exception: + return False + return True + + self.wait_for_predicate(get_uacomment) + + def check_single_miner(self): + scenario_file = self.scen_dir / "miner_std.py" + self.log.info(f"Running scenario from: {scenario_file}") + # Mine from a tank that is not first or last and + # is one of the only few in the network that even + # has rpc reatewallet method! + self.warnet(f"run {scenario_file} --tank=tank-0026 --interval=1") + self.wait_for_predicate( + lambda: int(self.warnet("bitcoin rpc tank-0026 getblockcount")) >= 10 + ) + running = scenarios_deployed() + assert len(running) == 1, f"Expected one running scenario, got {len(running)}" + assert running[0]["status"] == "running", "Scenario should be running" + stop_scenario(running[0]["name"]) + self.wait_for_all_scenarios() + + +if __name__ == "__main__": + test = ConfTest() + test.run_test() diff --git a/test/dag_connection_test.py b/test/dag_connection_test.py index 32c2ccc8c..dee38356a 100755 --- a/test/dag_connection_test.py +++ b/test/dag_connection_test.py @@ -9,42 +9,27 @@ class DAGConnectionTest(TestBase): def __init__(self): super().__init__() - self.graph_file_path = ( - Path(os.path.dirname(__file__)) / "data" / "ten_semi_unconnected.graphml" - ) + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "ten_semi_unconnected" + self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" def run_test(self): - self.start_server() try: self.setup_network() self.run_connect_dag_scenario() - self.run_connect_dag_scenario_post_connection() finally: - self.stop_server() + self.cleanup() def setup_network(self): self.log.info("Setting up network") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) + self.log.info(self.warnet(f"deploy {self.network_dir}")) self.wait_for_all_tanks_status(target="running") self.wait_for_all_edges() def run_connect_dag_scenario(self): - self.log.info("Running connect_dag scenario") - self.log_expected_msgs = [ - "Successfully ran the connect_dag.py scenario using a temporary file" - ] - self.log_unexpected_msgs = ["Test failed."] - self.warcli("scenarios run-file test/data/scenario_connect_dag.py") + scenario_file = self.scen_dir / "test_scenarios" / "connect_dag.py" + self.log.info(f"Running scenario from: {scenario_file}") + self.warnet(f"run {scenario_file} --source_dir={self.scen_dir}") self.wait_for_all_scenarios() - self.assert_log_msgs() - - def run_connect_dag_scenario_post_connection(self): - self.log.info("Running connect_dag scenario") - self.log_expected_msgs = ["Successfully ran the connect_dag.py scenario"] - self.log_unexpected_msgs = ["Test failed"] - self.warcli("scenarios run-file test/data/scenario_connect_dag.py") - self.wait_for_all_scenarios() - self.assert_log_msgs() if __name__ == "__main__": diff --git a/test/data/12_node_ring.graphml b/test/data/12_node_ring.graphml deleted file mode 100644 index 7cdf3a7f7..000000000 --- a/test/data/12_node_ring.graphml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - 27.0 - -uacomment=w0 -debug=validation - - - 27.0 - -uacomment=w1 -debug=validation - - - 27.0 - -uacomment=w2 -debug=validation - - - 27.0 - -uacomment=w3 - - - 27.0 - -uacomment=w4 - - - 27.0 - -uacomment=w5 - - - 27.0 - -uacomment=w6 - - - 27.0 - -uacomment=w7 - - - 27.0 - -uacomment=w8 - - - 27.0 - -uacomment=w9 - - - 27.0 - -uacomment=w10 - - - 27.0 - - - - - - - - - - - - - - - - - diff --git a/test/data/12_node_ring/network.yaml b/test/data/12_node_ring/network.yaml new file mode 100644 index 000000000..62110f3d7 --- /dev/null +++ b/test/data/12_node_ring/network.yaml @@ -0,0 +1,61 @@ +nodes: + - name: tank-0000 + addnode: + - tank-0001 + config: | + debug=rpc + debug=validation + - name: tank-0001 + addnode: + - tank-0002 + config: | + debug=net + debug=validation + - name: tank-0002 + addnode: + - tank-0003 + config: | + debug=validation + - name: tank-0003 + addnode: + - tank-0004 + config: | + debug=validation + - name: tank-0004 + addnode: + - tank-0005 + - name: tank-0005 + addnode: + - tank-0006 + config: | + debug=validation + - name: tank-0006 + addnode: + - tank-0007 + - name: tank-0007 + config: | + debug=validation + addnode: + - tank-0008 + config: | + debug=validation + - name: tank-0008 + addnode: + - tank-0009 + config: | + debug=validation + - name: tank-0009 + addnode: + - tank-0010 + config: | + debug=validation + - name: tank-0010 + addnode: + - tank-0011 + config: | + debug=validation + - name: tank-0011 + addnode: + - tank-0000 + config: | + debug=validation \ No newline at end of file diff --git a/test/data/12_node_ring/node-defaults.yaml b/test/data/12_node_ring/node-defaults.yaml new file mode 100644 index 000000000..7e021cad1 --- /dev/null +++ b/test/data/12_node_ring/node-defaults.yaml @@ -0,0 +1,4 @@ +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" diff --git a/test/data/LN_100.json b/test/data/LN_100.json index 2087c74d8..d40575803 100644 --- a/test/data/LN_100.json +++ b/test/data/LN_100.json @@ -1,22 +1,20 @@ { - "directed": false, - "multigraph": false, - "graph": {}, "nodes": [ { - "last_update": 1710607733, - "alias": "BlueWave", + "pub_key": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "last_update": 1708144920, + "alias": "ln02.bullbitcoin.com", "addresses": [ { "network": "tcp", - "addr": "bh32yyu7dgyodzq3wdunho5esew5qbvrd2flsynqzeyilnd7jnyeahqd.onion:9735" + "addr": "172.81.180.3:9735" } ], - "color": "#d6de37", + "color": "#5140bf", "features": { - "0": { + "1": { "name": "data-loss-protect", - "is_required": true, + "is_required": false, "is_known": true }, "5": { @@ -29,14 +27,19 @@ "is_required": false, "is_known": true }, - "9": { + "8": { "name": "tlv-onion", - "is_required": false, + "is_required": true, "is_known": true }, - "12": { + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { "name": "static-remote-key", - "is_required": true, + "is_required": false, "is_known": true }, "14": { @@ -49,50 +52,49 @@ "is_required": false, "is_known": true }, - "23": { - "name": "anchors-zero-fee-htlc-tx", - "is_required": false, - "is_known": true - }, "27": { "name": "shutdown-any-segwit", "is_required": false, "is_known": true }, - "31": { - "name": "amp", + "45": { + "name": "explicit-commitment-type", "is_required": false, "is_known": true }, - "45": { - "name": "explicit-commitment-type", + "47": { + "name": "scid-alias", "is_required": false, "is_known": true }, - "55": { - "name": "keysend", + "51": { + "name": "zero-conf", "is_required": false, "is_known": true }, - "2023": { - "name": "script-enforced-lease", + "55": { + "name": "keysend", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6" + "custom_records": {} }, { - "last_update": 1702845433, - "alias": "03796678b7111abef10f", + "pub_key": "03e114a6bffa9ff420eb84491f3eceb3b2a5a5638f6d60d4324e16436c61acdcdc", + "last_update": 1710759555, + "alias": "zuragia", "addresses": [ { "network": "tcp", - "addr": "kgrxo24cm2ykhfazqer7bxzezcevrmir2du455o4mfbg3rpvqubpaqqd.onion:9735" + "addr": "185.21.223.27:9735" + }, + { + "network": "tcp", + "addr": "2qee6j4z7vnvq4q4z7h7s4czte3kgjshnrvuoikdhu2kf2gtf3b4xjad.onion:9735" } ], - "color": "#3399ff", + "color": "#77d133", "features": { "0": { "name": "data-loss-protect", @@ -149,34 +151,29 @@ "is_required": false, "is_known": true }, - "55": { - "name": "keysend", - "is_required": false, - "is_known": true - }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03796678b7111abef10f3ff85b88f81f9cfe81cac7e3628a11af1679ed912757d5" + "custom_records": {} }, { - "last_update": 1709578090, - "alias": "REDWHISPER", + "pub_key": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "last_update": 1710667233, + "alias": "Fourth Turning \u267b\ufe0f LND", "addresses": [ { "network": "tcp", - "addr": "piwsugtc6hr3jukavptmv6tjrxt2wt2i2q35lvjx6lk25vwvi43zicqd.onion:9735" + "addr": "ge6wzwqraphpvmby3tturziuky3awlj7xui3bc2mqihxxtm5dahiq6id.onion:9735" } ], - "color": "#023bb9", + "color": "#ff9802", "features": { - "1": { + "0": { "name": "data-loss-protect", - "is_required": false, + "is_required": true, "is_known": true }, "5": { @@ -189,19 +186,14 @@ "is_required": false, "is_known": true }, - "8": { + "9": { "name": "tlv-onion", - "is_required": true, - "is_known": true - }, - "11": { - "name": "unknown", "is_required": false, - "is_known": false + "is_known": true }, - "13": { + "12": { "name": "static-remote-key", - "is_required": false, + "is_required": true, "is_known": true }, "14": { @@ -219,50 +211,50 @@ "is_required": false, "is_known": true }, - "25": { - "name": "unknown", + "23": { + "name": "anchors-zero-fee-htlc-tx", "is_required": false, - "is_known": false + "is_known": true }, "27": { "name": "shutdown-any-segwit", "is_required": false, "is_known": true }, - "45": { - "name": "explicit-commitment-type", + "31": { + "name": "amp", "is_required": false, "is_known": true }, - "47": { - "name": "scid-alias", + "45": { + "name": "explicit-commitment-type", "is_required": false, "is_known": true }, - "51": { - "name": "zero-conf", + "55": { + "name": "keysend", "is_required": false, "is_known": true }, - "55": { - "name": "keysend", + "2023": { + "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "023bb9937dea9707583e45aa6708af0c5d16d9ca3970f67ab76606f43b7457309d" + "custom_records": {} }, { - "last_update": 1710772374, - "alias": "fr33node", + "pub_key": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "last_update": 1710856043, + "alias": "strike", "addresses": [ { "network": "tcp", - "addr": "ubzmye2tytcqarycr3jsirl3ajxallddfpvorc4t2odao4y5yovdalad.onion:9735" + "addr": "34.138.210.19:9735" } ], - "color": "#68f442", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -299,6 +291,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -325,23 +322,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "02e45ec6486ad8d2b2dcfe3e994f89035f7f1975e800bfe56f030d910647601199" + "custom_records": {} }, { - "last_update": 1710847816, - "alias": "LQwD-Canada", + "pub_key": "02b9735190f3e98ee3d96f0cef0bcd02af2ee37141d5c2d969683b354e2293fd8f", + "last_update": 1710808183, + "alias": "\ud83d\udc08 MediumOfExchange \ud83d\udc08\u200d\u2b1b", "addresses": [ { "network": "tcp", - "addr": "192.243.215.102:9735" - }, - { - "network": "tcp", - "addr": "yit5nizyrgk4n2gx7hkadaas5iz33kn6wulr5vdndzpbae6tykqdw7ad.onion:9735" + "addr": "ld7l2ckvvtpj4wev4vegmj3yl5fsd2uuhcy5gesxhdjihmdx3afvn2ad.onion:9735" } ], - "color": "#3399ff", + "color": "#0000cd", "features": { "0": { "name": "data-loss-protect", @@ -378,11 +371,6 @@ "is_required": false, "is_known": true }, - "19": { - "name": "wumbo-channels", - "is_required": false, - "is_known": true - }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -403,8 +391,8 @@ "is_required": false, "is_known": true }, - "47": { - "name": "scid-alias", + "55": { + "name": "keysend", "is_required": false, "is_known": true }, @@ -414,19 +402,27 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1" + "custom_records": {} }, { - "last_update": 1710855669, - "alias": "River Financial 2", + "pub_key": "021d2436cab847373a4212bf6d754ead5304f5d0791479643893a837b295f3441c", + "last_update": 1710856205, + "alias": "DorianIsMySatoshi", "addresses": [ { "network": "tcp", - "addr": "34.136.230.235:9735" + "addr": "45.142.235.46:9735" + }, + { + "network": "tcp", + "addr": "[2a10:3781:2c19::1]:9735" + }, + { + "network": "tcp", + "addr": "wqbh5jxqgbfxhb7wgjdfzkfcihkgcbvyzivme46x2yrqkdjghx6wesid.onion:9735" } ], - "color": "#ff9900", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -494,16 +490,20 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5" + "custom_records": {} }, { - "last_update": 1707020283, - "alias": "027d5369f807773ed75d", + "pub_key": "036fe0795980588a1489d2ddf11f47a0ac36671e2cbff46bc4ace52738f15a1956", + "last_update": 1710797125, + "alias": "NoGoodNode", "addresses": [ { "network": "tcp", - "addr": "leb7bxhf3ppdfssy2bu4udc2cnhbehw5nhkkiumh76fhxel6bqtqmqqd.onion:9735" + "addr": "170.75.163.98:9735" + }, + { + "network": "tcp", + "addr": "tk5urwrimyqdm36mfdiqarv2i7uoirqjazvpeqypmxdhtpiefwxhypyd.onion:9735" } ], "color": "#3399ff", @@ -543,6 +543,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -563,34 +568,25 @@ "is_required": false, "is_known": true }, - "55": { - "name": "keysend", - "is_required": false, - "is_known": true - }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "027d5369f807773ed75dc87dd12c585210b72827263f4a1009f052e67c12ac92f5" + "custom_records": {} }, { - "last_update": 1710847881, - "alias": "lnd1.relampago.cash", + "pub_key": "0246ee8e4c965296799eebd29a0948b9a4641843298b0f2a8e42256c4b594e4b8f", + "last_update": 1710791376, + "alias": "LightningStation", "addresses": [ { "network": "tcp", - "addr": "44.225.98.1:9735" - }, - { - "network": "tcp", - "addr": "6hmm7rvxv6mu7sv5wetqfigjljrde2cd6kr474ncylbbfzdsczyvhlad.onion:9735" + "addr": "q3vax5ytpca2toncaoxbzzszque2iuhm2eu4l7a6nps4vhtbz2zhdwid.onion:9735" } ], - "color": "#ffa47a", + "color": "#ff3c00", "features": { "0": { "name": "data-loss-protect", @@ -663,19 +659,23 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d" + "custom_records": {} }, { - "last_update": 1710623243, - "alias": "bitpi", + "pub_key": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "last_update": 1710853945, + "alias": "Rust-eze (now BoltTrustiC)", "addresses": [ { "network": "tcp", - "addr": "iddfruwmorrrlr32akea422ajghfyicyohdd3n3a6mmiigd5x7druhyd.onion:9735" + "addr": "46.105.76.211:9735" + }, + { + "network": "tcp", + "addr": "wama6rprmtwzsmqyhcasko5q2ypov5apqa4ntlzraxpcgu54ewdkuhid.onion:9735" } ], - "color": "#68f442", + "color": "#b5603f", "features": { "0": { "name": "data-loss-protect", @@ -712,13 +712,13 @@ "is_required": false, "is_known": true }, - "23": { - "name": "anchors-zero-fee-htlc-tx", + "19": { + "name": "wumbo-channels", "is_required": false, "is_known": true }, - "27": { - "name": "shutdown-any-segwit", + "23": { + "name": "anchors-zero-fee-htlc-tx", "is_required": false, "is_known": true }, @@ -732,25 +732,30 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3" + "custom_records": {} }, { - "last_update": 1674198664, - "alias": "Bitcomoon.com", + "pub_key": "03518cfddeb18d50db01fc316984bac961a6d978115a52259cd5d2e47fbe363e94", + "last_update": 1704834837, + "alias": "MontanasRaspiBlitz", "addresses": [ { "network": "tcp", - "addr": "e3xx3u5oxvuo4bgix5cl2ngfeyvexoz7my3rktgrpmf6gq3sryi2orqd.onion:9735" + "addr": "jlffjofzvlbxwfbxvpugkhiflq3pi5c4fgmstlio6rnlcjds3jpiitad.onion:9735" } ], - "color": "#79e0ed", + "color": "#68f442", "features": { "0": { "name": "data-loss-protect", @@ -787,6 +792,11 @@ "is_required": false, "is_known": true }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, "27": { "name": "shutdown-any-segwit", "is_required": false, @@ -802,25 +812,25 @@ "is_required": false, "is_known": true }, - "55": { - "name": "keysend", + "2023": { + "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "02bf145fe009fef1230f3f435b5e448db3470a1fdbff95d41cefa71b7d6f14fcda" + "custom_records": {} }, { - "last_update": 1710586588, - "alias": "Bitrequest", + "pub_key": "02404d14a1a5ffdf7f02a04b1aea3941a9ff3184b9ef2fd2c78af862cde9572188", + "last_update": 1710815461, + "alias": "Beinardus [LND]", "addresses": [ { "network": "tcp", - "addr": "wvhczhxeyuo5gsbc6ub5n66yxycvojyx26eunh76l3ta7ks7yjckbgqd.onion:9735" + "addr": "jt6lh76prisvqvt3lyeyy2b5kcmudq6ab2tli6l3rvlwfcxzqzbhk5yd.onion:9735" } ], - "color": "#0cc0ed", + "color": "#5050a0", "features": { "0": { "name": "data-loss-protect", @@ -857,6 +867,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -888,19 +903,27 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296" + "custom_records": {} }, { - "last_update": 1710773661, - "alias": "raspiblitz", + "pub_key": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "last_update": 1710856661, + "alias": "allNice | torq.co", "addresses": [ { "network": "tcp", - "addr": "n75lmwb3ykmwvbqajzkdh6otc6ukasij4f556ub54ug4qvs22rb3rsyd.onion:9735" + "addr": "141.95.72.206:9735" + }, + { + "network": "tcp", + "addr": "[2001:41d0:700:5fce::]:9735" + }, + { + "network": "tcp", + "addr": "sioo43tvkudx56ksir3ndwynv2df3356dpjc6s4yy3abjtcmsys6wnad.onion:9735" } ], - "color": "#68f442", + "color": "#660000", "features": { "0": { "name": "data-loss-protect", @@ -937,6 +960,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -957,22 +985,32 @@ "is_required": false, "is_known": true }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "02c87bce43398e73f65df561b4e776306fc1e74657d67131d76866ba617a0e63c2" + "custom_records": {} }, { - "last_update": 1710825523, - "alias": "qubi1", + "pub_key": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "last_update": 1710836058, + "alias": "getlipa.com mainnet", "addresses": [ { "network": "tcp", - "addr": "mkejl5jnqe4ovaow57l47hi46xbjcxnf54jriiwvs5rsxcxxjbo5b4yd.onion:9735" + "addr": "62.171.183.114:9735" } ], "color": "#3399ff", @@ -1012,6 +1050,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -1032,27 +1075,26 @@ "is_required": false, "is_known": true }, - "55": { - "name": "keysend", - "is_required": false, - "is_known": true - }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "02b196899ff33d892562c557a37f56a3702f14eba8aee8585625ed8cf4217f2ab3" + "custom_records": {} }, { - "last_update": 1710850518, - "alias": "Knoten", + "pub_key": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "last_update": 1710829358, + "alias": "MUTATRUM", "addresses": [ { "network": "tcp", - "addr": "xetu2kpbvonq2urqpadclyzy76gwhdrkz6lgazz65j66leapw4bmpoad.onion:9735" + "addr": "95.168.173.100:9735" + }, + { + "network": "tcp", + "addr": "qxlc2q5enou7vjuaa2g243fkoqg6w24biqtr7zrqlxprdhj2tqzzbsad.onion:9735" } ], "color": "#68f442", @@ -1092,6 +1134,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -1112,25 +1159,38 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96" + "custom_records": {} }, { - "last_update": 1710568425, - "alias": "Viva Bitcoin", + "pub_key": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "last_update": 1710856592, + "alias": "LNB\u03dfG [Edge-2]", "addresses": [ { "network": "tcp", - "addr": "2ti2gy74nlyjsgcs42wemjwd3skmndbbv776ufldkngbzmpo7tbsntyd.onion:9735" + "addr": "46.229.165.140:9735" + }, + { + "network": "tcp", + "addr": "[2a02:b48:207:2:9735:9735:1:2]:9735" + }, + { + "network": "tcp", + "addr": "mw2d3bedftuixrqgl4qsstqzz55k5g6ionuo2arab7bqyc2mxoihwsad.onion:9735" } ], - "color": "#ffff00", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -1167,6 +1227,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -1192,20 +1257,30 @@ "is_required": false, "is_known": true }, + "181": { + "name": "simple-taproot-chans-x", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "02aeb397b7212dc1de5252684f9a1f1957d8bcc9864193b538ce78f9558685d666" + "custom_records": {} }, { - "last_update": 1710394153, - "alias": "", - "addresses": [], - "color": "#007f00", + "pub_key": "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa", + "last_update": 1710834273, + "alias": "not benthecarman", + "addresses": [ + { + "network": "tcp", + "addr": "185.150.162.100:9735" + } + ], + "color": "#f2a900", "features": { "0": { "name": "data-loss-protect", @@ -1222,9 +1297,9 @@ "is_required": false, "is_known": true }, - "8": { + "9": { "name": "tlv-onion", - "is_required": true, + "is_required": false, "is_known": true }, "12": { @@ -1247,11 +1322,26 @@ "is_required": false, "is_known": true }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, "27": { "name": "shutdown-any-segwit", "is_required": false, "is_known": true }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "39": { + "name": "unknown", + "is_required": false, + "is_known": false + }, "45": { "name": "explicit-commitment-type", "is_required": false, @@ -1262,25 +1352,34 @@ "is_required": false, "is_known": true }, - "51": { - "name": "zero-conf", - "is_required": false, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20" + "custom_records": {} }, { - "last_update": 1710769806, - "alias": "EcoNode", + "pub_key": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "last_update": 1710856043, + "alias": "IMF_SUCKS", "addresses": [ { "network": "tcp", - "addr": "kpa3fv64oxsgl7ymsmu27t5wwe4vpx47wer3tr7fk5ateeskmoh6hiqd.onion:9735" + "addr": "91.117.204.128:9735" + }, + { + "network": "tcp", + "addr": "jz4ydh5i3aajj7ib7h4wupn7vjybegywioh7mwyrpssfwtjmowkxnaad.onion:9735" } ], - "color": "#3399ff", + "color": "#ffff00", "features": { "0": { "name": "data-loss-protect", @@ -1317,6 +1416,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -1348,19 +1452,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "0202b5d54467d075893d46b6205067bbab2bcbc632513cd15661ad803d2dcb4ce3" + "custom_records": {} }, { - "last_update": 1707656697, - "alias": "02f5be5f3d54b66bf531", + "pub_key": "037ff12b6a4e4bcb4b944b6d20af08cdff61b3461c1dff0d00a88697414d891bc7", + "last_update": 1710846456, + "alias": "\ud83e\udd16RoboSats\u26a1DevFund\ud83d\udcbb\ud83c\udf75", "addresses": [ { "network": "tcp", - "addr": "vedvjiibjrbbaaegvjqvx4thfobbj7gs5scb5gtpvxyb3t423ao23sad.onion:9735" + "addr": "6ivt774h43velhemo4uhdcxdojnwt6k2vfxzpvr4ya4ivr77d522jtid.onion:9735" } ], - "color": "#3399ff", + "color": "#1976d2", "features": { "0": { "name": "data-loss-protect", @@ -1397,6 +1501,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -1417,25 +1526,30 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "02f5be5f3d54b66bf531b96f06327f59cd139f6e2c301cc0131a3f632025c2704c" + "custom_records": {} }, { - "last_update": 1710693115, - "alias": "lightning-roulette.com", + "pub_key": "03bb086b83afa45795c2265ebc76dcc749a590bcd83ba244c27d4f47b1102ffc60", + "last_update": 1710487611, + "alias": "iaconide", "addresses": [ { "network": "tcp", - "addr": "34.65.32.189:9735" + "addr": "34wnmpnzr5an4volnsthuklsk7n6xl5v63dca6ffvtbzbmekddyw3wad.onion:9735" } ], - "color": "#11ff22", + "color": "#196fc5", "features": { "0": { "name": "data-loss-protect", @@ -1472,11 +1586,6 @@ "is_required": false, "is_known": true }, - "19": { - "name": "wumbo-channels", - "is_required": false, - "is_known": true - }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -1503,19 +1612,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403" + "custom_records": {} }, { - "last_update": 1662699597, - "alias": "BambergCityNode5", + "pub_key": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "last_update": 1710807593, + "alias": "OpenNode.com", "addresses": [ { "network": "tcp", - "addr": "tfsmtkznl6spgnm5j3xxa6wmawvmi4ipgbvuazdngy6xqbigqdiojpid.onion:9735" + "addr": "3.132.230.42:9735" } ], - "color": "#cd6d5f", + "color": "#000000", "features": { "0": { "name": "data-loss-protect", @@ -1551,54 +1660,14 @@ "name": "multi-path-payments", "is_required": false, "is_known": true - } - }, - "custom_records": {}, - "pub_key": "024a65932bdad1c4ef070289a6dd9f71b315e61d871b38c79339d0cc3d26e100e1" - }, - { - "last_update": 1710392585, - "alias": "", - "addresses": [], - "color": "#007f00", - "features": { - "0": { - "name": "data-loss-protect", - "is_required": true, - "is_known": true - }, - "5": { - "name": "upfront-shutdown-script", - "is_required": false, - "is_known": true - }, - "7": { - "name": "gossip-queries", - "is_required": false, - "is_known": true - }, - "8": { - "name": "tlv-onion", - "is_required": true, - "is_known": true }, - "12": { - "name": "static-remote-key", - "is_required": true, - "is_known": true - }, - "14": { - "name": "payment-addr", - "is_required": true, - "is_known": true - }, - "17": { - "name": "multi-path-payments", + "19": { + "name": "wumbo-channels", "is_required": false, "is_known": true }, - "19": { - "name": "wumbo-channels", + "23": { + "name": "anchors-zero-fee-htlc-tx", "is_required": false, "is_known": true }, @@ -1607,35 +1676,35 @@ "is_required": false, "is_known": true }, - "45": { - "name": "explicit-commitment-type", + "31": { + "name": "amp", "is_required": false, "is_known": true }, - "47": { - "name": "scid-alias", + "45": { + "name": "explicit-commitment-type", "is_required": false, "is_known": true }, - "51": { - "name": "zero-conf", + "2023": { + "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17" + "custom_records": {} }, { - "last_update": 1710817951, - "alias": "bblocker21", + "pub_key": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "last_update": 1710847535, + "alias": "nodorobico", "addresses": [ { "network": "tcp", - "addr": "l4nxfimtxjjerzewyb4ihp4jhiauw2madq3krdnqz4s3x52kmya76lyd.onion:9735" + "addr": "vbkuzms43v6wnwyq2v2exrlzimx3lzfbtvmprwv6q4w6zk23g3dzueid.onion:9735" } ], - "color": "#ffb233", + "color": "#000000", "features": { "0": { "name": "data-loss-protect", @@ -1703,10 +1772,10 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8" + "custom_records": {} }, { + "pub_key": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", "last_update": 1710847967, "alias": "Bitrefill", "addresses": [ @@ -1783,23 +1852,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + "custom_records": {} }, { - "last_update": 1710767460, - "alias": "lnd.cryptoassets.co.za", + "pub_key": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "last_update": 1710856657, + "alias": "WalletOfSatoshi.com", "addresses": [ { "network": "tcp", - "addr": "197.155.6.194:9735" - }, - { - "network": "tcp", - "addr": "xiigglrrhl6gl55g6ndou5pkg7y7gxmln6bs6eov2w5od2xavdfukrqd.onion:9735" + "addr": "170.75.163.209:9735" } ], - "color": "#000000", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -1861,30 +1926,20 @@ "is_required": false, "is_known": true }, - "55": { - "name": "keysend", - "is_required": false, - "is_known": true - }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "021227d4be948ab84613d5dbd47b6e148cfb811432d27eb92e80d28382b1b27d27" + "custom_records": {} }, { - "last_update": 1710821651, - "alias": "OpenNode.com", - "addresses": [ - { - "network": "tcp", - "addr": "18.222.70.85:9735" - } - ], - "color": "#000000", + "pub_key": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17", + "last_update": 1710392585, + "alias": "", + "addresses": [], + "color": "#007f00", "features": { "0": { "name": "data-loss-protect", @@ -1901,9 +1956,9 @@ "is_required": false, "is_known": true }, - "9": { + "8": { "name": "tlv-onion", - "is_required": false, + "is_required": true, "is_known": true }, "12": { @@ -1926,42 +1981,41 @@ "is_required": false, "is_known": true }, - "23": { - "name": "anchors-zero-fee-htlc-tx", - "is_required": false, - "is_known": true - }, "27": { "name": "shutdown-any-segwit", "is_required": false, "is_known": true }, - "31": { - "name": "amp", + "45": { + "name": "explicit-commitment-type", "is_required": false, "is_known": true }, - "45": { - "name": "explicit-commitment-type", + "47": { + "name": "scid-alias", "is_required": false, "is_known": true }, - "2023": { - "name": "script-enforced-lease", + "51": { + "name": "zero-conf", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + "custom_records": {} }, { - "last_update": 1710836095, - "alias": "021cb32426ed1a6be953", + "pub_key": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "last_update": 1710845923, + "alias": "OLYMPUS by ZEUS", "addresses": [ { "network": "tcp", - "addr": "g2vldjytfdx4r2hdmgg3ccqrxczxgxl4e3w7rr3emvp4br4xcoo4void.onion:9735" + "addr": "45.79.192.236:9735" + }, + { + "network": "tcp", + "addr": "r46dwvxcdri754hf6n3rwexmc53h5x4natg5g6hidnxfzejm5xrqn2id.onion:9735" } ], "color": "#3399ff", @@ -2001,6 +2055,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -2021,25 +2080,53 @@ "is_required": false, "is_known": true }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "181": { + "name": "simple-taproot-chans-x", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000" + "custom_records": {} }, { - "last_update": 1710776023, - "alias": "Fastlightning", + "pub_key": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "last_update": 1710823738, + "alias": "lnmarkets.com", "addresses": [ { "network": "tcp", - "addr": "tspdnxsdk77b6wdly6lujqr724hoq23buuez3roy6xo5ausv3riisbqd.onion:9735" + "addr": "3.95.117.200:9735" + }, + { + "network": "tcp", + "addr": "172.81.0.102:9735" + }, + { + "network": "tcp", + "addr": "heoznd3w4xvmiiihykdhviegejnvhzgoyfzhucx5n2cgvlsp3fkpvpad.onion:9735" } ], - "color": "#3399ff", + "color": "#327aff", "features": { "0": { "name": "data-loss-protect", @@ -2076,6 +2163,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -2096,25 +2188,34 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03919a0a495cfd08779a3c23168827243cabe597e8ee5ea0c7827b6c407e260fe2" + "custom_records": {} }, { - "last_update": 1710845779, - "alias": "LightningPremium", + "pub_key": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "last_update": 1710838956, + "alias": "Boltz", "addresses": [ { "network": "tcp", - "addr": "5u3crvcbydktdcwsk5utapo5vvd7s2uzkn45u7kemusvgikwqydytjad.onion:9735" + "addr": "45.86.229.190:9735" + }, + { + "network": "tcp", + "addr": "d7kak4gpnbamm3b4ufq54aatgm3alhx3jwmu6kyy2bgjaauinkipz3id.onion:9735" } ], - "color": "#68f442", + "color": "#ff9800", "features": { "0": { "name": "data-loss-protect", @@ -2187,19 +2288,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac" + "custom_records": {} }, { - "last_update": 1648917357, - "alias": "mynodebtc.cooper", + "pub_key": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "last_update": 1710821053, + "alias": "flypdev", "addresses": [ { "network": "tcp", - "addr": "bh4ooqtd3mngj4vk3m3gpqd3isaywi67ed5iochdg3lhexzikc56epad.onion:9735" + "addr": "54.194.246.117:9735" } ], - "color": "#68f442", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -2236,11 +2337,21 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, "is_known": true }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, "31": { "name": "amp", "is_required": false, @@ -2257,19 +2368,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "03d213f4115a55f0d0ab185434fcb6d3f119fdede11e901fe57f8b91d57ed316e6" + "custom_records": {} }, { - "last_update": 1710852615, - "alias": "Decentralized", + "pub_key": "026f46207fd290a33cbd86e29b3ad0a47cdd44ab9aa5267cde66483e10aa9d3180", + "last_update": 1710847116, + "alias": "Authenticity", "addresses": [ { "network": "tcp", - "addr": "sgcq35suws6ft63egprwsncj357yt355rsa3aqqpjjimma4nfjhzdyqd.onion:9735" + "addr": "m3beyfsudvoujj6yrvx4ubvjqcxnq3cosanllxuidq55ezji2zwhw2ad.onion:9735" } ], - "color": "#61f5b2", + "color": "#ff5000", "features": { "0": { "name": "data-loss-protect", @@ -2306,6 +2417,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -2326,6 +2442,16 @@ "is_required": false, "is_known": true }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, "55": { "name": "keysend", "is_required": false, @@ -2337,23 +2463,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3" + "custom_records": {} }, { - "last_update": 1710551722, - "alias": "02abb5e57ff442770d9d", + "pub_key": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "last_update": 1710809830, + "alias": "bfx-lnd0", "addresses": [ { "network": "tcp", - "addr": "172.67.158.44:9735" - }, - { - "network": "tcp", - "addr": "rjufmivql2si355mfczbg2wirc24rnjngpo5hsotmabiafuewojkbdqd.onion:9735" + "addr": "34.65.85.39:9735" } ], - "color": "#3399ff", + "color": "#16b157", "features": { "0": { "name": "data-loss-protect", @@ -2421,19 +2543,23 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "02abb5e57ff442770d9dc1d1ab66f45b015ac7aa033e4407051d060a471b71b08b" + "custom_records": {} }, { - "last_update": 1710852249, - "alias": "Bitrefill Routing", + "pub_key": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "last_update": 1710847816, + "alias": "LQwD-Canada", "addresses": [ { "network": "tcp", - "addr": "54.77.250.40:9735" + "addr": "192.243.215.102:9735" + }, + { + "network": "tcp", + "addr": "yit5nizyrgk4n2gx7hkadaas5iz33kn6wulr5vdndzpbae6tykqdw7ad.onion:9735" } ], - "color": "#ff001c", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -2495,29 +2621,30 @@ "is_required": false, "is_known": true }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + "custom_records": {} }, { - "last_update": 1710811679, - "alias": "bleskomat", + "pub_key": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "last_update": 1710723427, + "alias": "fixedfloat.com", "addresses": [ { "network": "tcp", - "addr": "165.227.161.245:9735" - }, - { - "network": "tcp", - "addr": "5aggdumr4x3qkx5ptshggvrdkgaxkpsklw4nwmnsa6diby6vwcvccyad.onion:9735" + "addr": "185.5.53.91:9735" } ], - "color": "#3399ff", + "color": "#053456", "features": { "0": { "name": "data-loss-protect", @@ -2554,6 +2681,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -2580,16 +2712,20 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0" + "custom_records": {} }, { - "last_update": 1710852256, - "alias": "Liaoning", + "pub_key": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "last_update": 1710279837, + "alias": "node02.fulmo.org", "addresses": [ { "network": "tcp", - "addr": "vdhhkanmkfr7x2mlkbo7lah6sh7mhwxqt7gv73m44pkfk4y537tg54yd.onion:9735" + "addr": "62.171.165.119:9735" + }, + { + "network": "tcp", + "addr": "4wstfgqbt347r4p5sq3yjrqhfntxlxh5y5ludah6wa6px7norowcgnyd.onion:9735" } ], "color": "#3399ff", @@ -2629,6 +2765,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -2649,25 +2790,30 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880" + "custom_records": {} }, { - "last_update": 1710848892, - "alias": "shtyrlitz", + "pub_key": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "last_update": 1710602731, + "alias": "Brrrrrak", "addresses": [ { "network": "tcp", - "addr": "aswabsuhftuqagrvxlhzc5la73wj5skg7shp4vjpwgialbcgkceby6qd.onion:9735" + "addr": "63.33.31.120:9735" } ], - "color": "#68f442", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -2704,8 +2850,13 @@ "is_required": false, "is_known": true }, - "23": { - "name": "anchors-zero-fee-htlc-tx", + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", "is_required": false, "is_known": true }, @@ -2735,19 +2886,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "0328946a0693c2a4dd7e5afc2ce2e020c54e07d6693a293b4e1c1fd7e2d09a3eeb" + "custom_records": {} }, { - "last_update": 1710803334, - "alias": "FinanzielleFreiheit", + "pub_key": "03fe320dd1a656842bf4cf31e25fbe76f18ae1d3ac35c477e32063b360024fd956", + "last_update": 1710854956, + "alias": "DinheiroReal", "addresses": [ { "network": "tcp", - "addr": "valdvygv5w6bfnza4yfshqsvljhv7grgyq4wgpke2jwvugfretiqwcqd.onion:9735" + "addr": "6yt5b3xefj6js2qc3skyuvo3t6t63wijxismzvt674gbl4pqonjtwnad.onion:9735" } ], - "color": "#68f442", + "color": "#3399ff", "features": { "0": { "name": "data-loss-protect", @@ -2804,29 +2955,38 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03ea17fd97ca7382a192dd4533c8e95ca946ef0827b14984e682af699bcaeb5a53" + "custom_records": {} }, { - "last_update": 1710805061, - "alias": "lndwr2.zaphq.io", + "pub_key": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "last_update": 1710815967, + "alias": "Voltage Flow 2.0", "addresses": [ { "network": "tcp", - "addr": "34.74.114.254:9735" + "addr": "52.88.33.119:9735" + }, + { + "network": "tcp", + "addr": "m33ascvukogaknee5w2tahp4xzhtnotx3a3m2ipkavxeggeiia2otqad.onion:9735" } ], - "color": "#3399ff", + "color": "#ff5000", "features": { - "0": { + "1": { "name": "data-loss-protect", - "is_required": true, + "is_required": false, "is_known": true }, "5": { @@ -2839,14 +2999,19 @@ "is_required": false, "is_known": true }, - "9": { + "8": { "name": "tlv-onion", - "is_required": false, + "is_required": true, "is_known": true }, - "12": { + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { "name": "static-remote-key", - "is_required": true, + "is_required": false, "is_known": true }, "14": { @@ -2864,45 +3029,50 @@ "is_required": false, "is_known": true }, - "23": { - "name": "anchors-zero-fee-htlc-tx", + "25": { + "name": "unknown", "is_required": false, - "is_known": true + "is_known": false }, "27": { "name": "shutdown-any-segwit", "is_required": false, "is_known": true }, - "31": { - "name": "amp", + "45": { + "name": "explicit-commitment-type", "is_required": false, "is_known": true }, - "45": { - "name": "explicit-commitment-type", + "47": { + "name": "scid-alias", "is_required": false, "is_known": true }, - "2023": { - "name": "script-enforced-lease", + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149" + "custom_records": {} }, { - "last_update": 1710856675, - "alias": "Civilization Phaze IV", + "pub_key": "03ff4d7532fc749c731de8bdac7226c06fdb82a458cd7ee557a6e355f5b359fd84", + "last_update": 1663608503, + "alias": "dm8try_btc.lnd [myNode]", "addresses": [ { "network": "tcp", - "addr": "ko4qvn23p2txecujtt5nv67mb2ikzhbebh2hs4rgfvonfpk2tqhux4id.onion:9735" + "addr": "qnbyr4dywmlb7iix76o4aoyng6grecaapqgzxs45x3x6t2jpxrljcjad.onion:9735" } ], - "color": "#ff8733", + "color": "#68f442", "features": { "0": { "name": "data-loss-protect", @@ -2944,11 +3114,6 @@ "is_required": false, "is_known": true }, - "27": { - "name": "shutdown-any-segwit", - "is_required": false, - "is_known": true - }, "31": { "name": "amp", "is_required": false, @@ -2959,34 +3124,33 @@ "is_required": false, "is_known": true }, - "55": { - "name": "keysend", - "is_required": false, - "is_known": true - }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "02841286ba5314ac9a4a29eb8d558b30fec348f22e22351ff8896288dede68e6a7" + "custom_records": {} }, { - "last_update": 1710771876, - "alias": "0209a06852f1257d70e5", + "pub_key": "03c41fde73d7853d07c0b0baf110e91c5f54f768cc94d58ddc0d3e947fa7d27e29", + "last_update": 1710616318, + "alias": "lightningcheckout.eu", "addresses": [ { "network": "tcp", - "addr": "94.130.220.96:9735" + "addr": "5.75.133.40:9735" + }, + { + "network": "tcp", + "addr": "mzxf2hwuajxgxobh3uohbr5xnzxluz4wqhzu55yqia2scjq4b7ahipqd.onion:9735" } ], - "color": "#3399ff", + "color": "#f2a900", "features": { - "0": { + "1": { "name": "data-loss-protect", - "is_required": true, + "is_required": false, "is_known": true }, "5": { @@ -2999,14 +3163,19 @@ "is_required": false, "is_known": true }, - "9": { + "8": { "name": "tlv-onion", - "is_required": false, + "is_required": true, "is_known": true }, - "12": { + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { "name": "static-remote-key", - "is_required": true, + "is_required": false, "is_known": true }, "14": { @@ -3019,45 +3188,69 @@ "is_required": false, "is_known": true }, - "23": { - "name": "anchors-zero-fee-htlc-tx", + "19": { + "name": "wumbo-channels", "is_required": false, "is_known": true }, + "25": { + "name": "unknown", + "is_required": false, + "is_known": false + }, "27": { "name": "shutdown-any-segwit", "is_required": false, "is_known": true }, - "31": { - "name": "amp", + "29": { + "name": "unknown", "is_required": false, - "is_known": true + "is_known": false + }, + "39": { + "name": "unknown", + "is_required": false, + "is_known": false }, "45": { "name": "explicit-commitment-type", "is_required": false, "is_known": true }, - "2023": { - "name": "script-enforced-lease", + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "0209a06852f1257d70e5cea44a0919871fa20859eb33a62445f774e9fe96247a75" + "custom_records": {} }, { - "last_update": 1710586096, - "alias": "PERLY", + "pub_key": "030a425f5c69a29db30f6740d4e7df8f5612ef9955078ef4497490015464733dc8", + "last_update": 1710850065, + "alias": "\ud83c\udfdb\ufe0fTempleOfSats\ud83c\udfdb\ufe0f", "addresses": [ { "network": "tcp", - "addr": "twtvqk7jiz3luzkt346q326u5owmo73lfn4yhhr5efskkr4autuf6vqd.onion:9735" + "addr": "128.199.66.191:36224" + }, + { + "network": "tcp", + "addr": "yqaj6k7qqfrzasjlunt2s4u35y7h2oc44lkgyobhe44xo5iy42iacmad.onion:9735" } ], - "color": "#9245ff", + "color": "#ce0b2d", "features": { "0": { "name": "data-loss-protect", @@ -3125,19 +3318,19 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "033f481fe0e9344228b58e0297162bfa8d648d5043c12b6323df5eac61bd39094c" + "custom_records": {} }, { - "last_update": 1700638994, - "alias": "UmbrelMCL", + "pub_key": "03db3386adf0e2221555a7f3fc7282eca60c5eabfed8e1789d591a9241c342c091", + "last_update": 1701328211, + "alias": "sanju", "addresses": [ { "network": "tcp", - "addr": "rlubaw7sxcaqmostf3e4x7yollfo4x2tzfwxrj3ssvxqusdes2q6k6yd.onion:9735" + "addr": "bwmdhddpb477aed2xdjdqathml3o7bxemsumgnl6tcxel7to7wop4pad.onion:9735" } ], - "color": "#3399ff", + "color": "#ffe233", "features": { "0": { "name": "data-loss-protect", @@ -3174,11 +3367,6 @@ "is_required": false, "is_known": true }, - "19": { - "name": "wumbo-channels", - "is_required": false, - "is_known": true - }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -3199,30 +3387,29 @@ "is_required": false, "is_known": true }, - "55": { - "name": "keysend", - "is_required": false, - "is_known": true - }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "0321743efb2d44a9c1ff8194f03728e119bbf6cde88201b9ac8e6e48f96c99d597" + "custom_records": {} }, { - "last_update": 1710736174, - "alias": "Laika", + "pub_key": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "last_update": 1710839517, + "alias": "IBEX_Ops1", "addresses": [ { "network": "tcp", - "addr": "tezwktlb2f3ah2tbla6emehwic4eelwyxhycfvzgzmc7n6jxz33ozead.onion:9735" + "addr": "18.221.65.244:9735" + }, + { + "network": "tcp", + "addr": "umtpb5erx6v7ouvomeg33lxpryw5ufnet5zywuqx6piwrvo5jyrd3qqd.onion:9735" } ], - "color": "#300fb0", + "color": "#044900", "features": { "0": { "name": "data-loss-protect", @@ -3259,6 +3446,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -3290,19 +3482,23 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "03b9de7a7d7e8a474b859a59e314cf2a143ae9c569ce78c46289b3ad16cb8c03f9" + "custom_records": {} }, { - "last_update": 1710803050, - "alias": "lndeu0.zaphq.io", + "pub_key": "022c6a25c6090a64b2710ef7f15740c4a000c480ef16a3b5845864af448cb322ba", + "last_update": 1710754632, + "alias": "sebastix", "addresses": [ { "network": "tcp", - "addr": "35.196.134.164:9735" + "addr": "fjqqno7cidbbfox4rrrpnvn4vikzhzh3kxk22si3ckhpuiuhdi3bssad.onion:9735" + }, + { + "network": "tcp", + "addr": "rrbmesbr5dhh3ru64c6dwjm7o4cevmuwf23wwuae3ge5drdbvwhzoiyd.onion:9735" } ], - "color": "#3399ff", + "color": "#ffa500", "features": { "0": { "name": "data-loss-protect", @@ -3364,19 +3560,29 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + "custom_records": {} }, { - "last_update": 1710790037, - "alias": "0294774ee02a9faa5a58", - "addresses": [], + "pub_key": "032261d6ec5fb578919e2455f2061d27bd93df57b26d78cdf7bad38f1368d0531b", + "last_update": 1710821064, + "alias": "LateStageAustriaHungary\ud83c\uddfa\ud83c\udde6", + "addresses": [ + { + "network": "tcp", + "addr": "nzlqmwcfqlsrhbis7dyaeagiotutdl7dmbndpotacrhkagz5xtx6dyqd.onion:9735" + } + ], "color": "#3399ff", "features": { "0": { @@ -3414,11 +3620,6 @@ "is_required": false, "is_known": true }, - "19": { - "name": "wumbo-channels", - "is_required": false, - "is_known": true - }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -3439,53 +3640,54 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "0294774ee02a9faa5a5870061f7f4833686184ad14a0b163c49442516c9edac1db" + "custom_records": {} }, { - "last_update": 1709977298, - "alias": "ACINQ", + "pub_key": "022ea68ac7588959a6a4829a5ff176c296329d614b90b3f5f4456b5d6130ea50f6", + "last_update": 1710853712, + "alias": "The Nodestrich \u267e\ufe0f", "addresses": [ { "network": "tcp", - "addr": "3.33.236.230:9735" - }, - { - "network": "tcp", - "addr": "of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad.onion:9735" + "addr": "6ozfljaeyguxijydmjlnbfpsra2kf5oo44wn3mhwwtcudiopxuqcfwqd.onion:9735" } ], - "color": "#49daaa", + "color": "#8e30eb", "features": { "0": { "name": "data-loss-protect", "is_required": true, "is_known": true }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, "7": { "name": "gossip-queries", "is_required": false, "is_known": true }, - "8": { + "9": { "name": "tlv-onion", - "is_required": true, - "is_known": true - }, - "11": { - "name": "unknown", "is_required": false, - "is_known": false + "is_known": true }, - "13": { + "12": { "name": "static-remote-key", - "is_required": false, + "is_required": true, "is_known": true }, "14": { @@ -3498,11 +3700,6 @@ "is_required": false, "is_known": true }, - "19": { - "name": "wumbo-channels", - "is_required": false, - "is_known": true - }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -3513,40 +3710,40 @@ "is_required": false, "is_known": true }, - "29": { - "name": "unknown", - "is_required": false, - "is_known": false - }, - "39": { - "name": "unknown", + "31": { + "name": "amp", "is_required": false, - "is_known": false + "is_known": true }, "45": { "name": "explicit-commitment-type", "is_required": false, "is_known": true }, - "47": { - "name": "scid-alias", + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "last_update": 1710849539, - "alias": "PeterJFrancoIII", + "pub_key": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "last_update": 1710651061, + "alias": "domesticblend", "addresses": [ { "network": "tcp", - "addr": "oyonslv2muhpngxl3yip3z7mt27auzlsx2luqydkxadludv2cva3thad.onion:9735" + "addr": "gn77lxmq52cphlsz33sajhuhgbgkp6gebcuciov2qkewfzmrzowqryqd.onion:9735" } ], - "color": "#3399ff", + "color": "#da1913", "features": { "0": { "name": "data-loss-protect", @@ -3583,6 +3780,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -3603,25 +3805,30 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + "custom_records": {} }, { - "last_update": 1710850693, - "alias": "Wallet Sat's", + "pub_key": "028a7930a96f7a604014a8d6874004262ea64aea4def90839c265f8c7ff8a4b34c", + "last_update": 1710768282, + "alias": "RaspiPuitz", "addresses": [ { "network": "tcp", - "addr": "4a2tjhiyz3rqgpfvragnk4uszv32dytzhapen46igyjlyzfpgix5zeqd.onion:9735" + "addr": "a7gya4fc3hhnymx76izyjd6l2nubdntxpnbn2plprdvjfljzio4sxvad.onion:9735" } ], - "color": "#3399ff", + "color": "#68f442", "features": { "0": { "name": "data-loss-protect", @@ -3684,23 +3891,23 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43" + "custom_records": {} }, { - "last_update": 1710617991, - "alias": "RUSHBNOSTOP", + "pub_key": "036dcba93cfc2d7294e586a658c90313d44fb742d832f0f79eb6e4361bd0afa707", + "last_update": 1673104053, + "alias": "YIRMIBIR", "addresses": [ { "network": "tcp", - "addr": "maeiws6uq6baeuim5q6mcavn3xpn4kkpaa5rv7uqhrs6wubhccnkn7ad.onion:9735" + "addr": "g7gw2gbnbaijq2ss2n2y22ifgi2feoamwjwbg2s4gc7phmb7ahx57zad.onion:9736" } ], - "color": "#68f442", + "color": "#036dcb", "features": { - "0": { + "1": { "name": "data-loss-protect", - "is_required": true, + "is_required": false, "is_known": true }, "5": { @@ -3713,14 +3920,19 @@ "is_required": false, "is_known": true }, - "9": { + "8": { "name": "tlv-onion", - "is_required": false, + "is_required": true, "is_known": true }, - "12": { + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { "name": "static-remote-key", - "is_required": true, + "is_required": false, "is_known": true }, "14": { @@ -3733,8 +3945,8 @@ "is_required": false, "is_known": true }, - "23": { - "name": "anchors-zero-fee-htlc-tx", + "21": { + "name": "anchor-commitments", "is_required": false, "is_known": true }, @@ -3743,7 +3955,91 @@ "is_required": false, "is_known": true }, - "31": { + "35": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "39": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "last_update": 1710835939, + "alias": "Tatooine", + "addresses": [ + { + "network": "tcp", + "addr": "159.69.32.62:22409" + }, + { + "network": "tcp", + "addr": "2la7tplwej7zaacie62hz4bfvtnxfmm64bnrf4d6bdyyl3bnte6w7qad.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { "name": "amp", "is_required": false, "is_known": true @@ -3753,22 +4049,27 @@ "is_required": false, "is_known": true }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + "custom_records": {} }, { - "last_update": 1710774982, - "alias": "sheesh", + "pub_key": "02ab80cce7e7dff00c964e4b2006eb4c8af4e71187572156a88275d3c126b90b66", + "last_update": 1707575341, + "alias": "albis", "addresses": [ { "network": "tcp", - "addr": "esowgqg2jhjzofvgkwdnyrozdvowvx7ymo3jsdh7oxxc74y7ga4egyyd.onion:9735" + "addr": "lycaicag653yzcehmguxdursh6k662xmcij5jnlavyyolvrvoph2esyd.onion:9735" } ], "color": "#68f442", @@ -3808,6 +4109,11 @@ "is_required": false, "is_known": true }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, "23": { "name": "anchors-zero-fee-htlc-tx", "is_required": false, @@ -3839,19 +4145,108 @@ "is_known": true } }, - "custom_records": {}, - "pub_key": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + "custom_records": {} }, { - "last_update": 1710855402, - "alias": "lndus0.zaphq.io", + "pub_key": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "last_update": 1709977298, + "alias": "ACINQ", "addresses": [ { "network": "tcp", - "addr": "34.138.228.220:9735" + "addr": "3.33.236.230:9735" + }, + { + "network": "tcp", + "addr": "of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad.onion:9735" } ], - "color": "#3399ff", + "color": "#49daaa", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { + "name": "static-remote-key", + "is_required": false, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "29": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "39": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03213c5175050fc1dfe0b4560c0cd5c6dd6bf4b25c2f11df0285384d0402e0d397", + "last_update": 1710821524, + "alias": "sats.mobi", + "addresses": [ + { + "network": "tcp", + "addr": "c2j5rin2zjhhsfe3oyj6p45vvczob33cvjqj55dwyltofh774s7hadad.onion:9735" + } + ], + "color": "#ff5000", "features": { "0": { "name": "data-loss-protect", @@ -3913,320 +4308,17689 @@ "is_required": false, "is_known": true }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, "2023": { "name": "script-enforced-lease", "is_required": false, "is_known": true } }, - "custom_records": {}, - "pub_key": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" - } - ], - "edges": [ - { - "channel_id": "821685930207739904", - "chan_point": "b8d52e4be913496dd31204b80a6214d30934fd77cf18d33f1f4b6f2b17aabc0a:0", - "last_update": 1710852888, - "capacity": "130000", - "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", - "disabled": false, - "max_htlc_msat": "128700000", - "last_update": 1710852888, - "custom_records": {} - }, - "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", - "disabled": false, - "max_htlc_msat": "128700000", - "last_update": 1710852885, - "custom_records": {} - }, - "custom_records": {}, - "node1_pub": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6", - "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" - }, - { - "channel_id": "851482695379255296", - "chan_point": "57457180438919be15029ad7763d11eb46d33057fe9ab02ca001439b48350e97:0", - "last_update": 1710451930, - "capacity": "300000", - "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", - "disabled": true, - "max_htlc_msat": "297000000", - "last_update": 1710451930, - "custom_records": {} - }, - "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "50", - "disabled": true, - "max_htlc_msat": "297000000", - "last_update": 1706955502, - "custom_records": {} - }, - "custom_records": {}, - "node1_pub": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6", - "node2_pub": "027d5369f807773ed75dc87dd12c585210b72827263f4a1009f052e67c12ac92f5" + "custom_records": {} }, { - "channel_id": "852238059856199681", - "chan_point": "48442adc84d9f96ea82a15d24eb5eed6ea66a8b633296a7a0f7cd0adebbf4685:1", - "last_update": 1710705189, - "capacity": "500000", - "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", - "disabled": false, - "max_htlc_msat": "495000000", - "last_update": 1710617530, - "custom_records": {} - }, - "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", - "disabled": false, - "max_htlc_msat": "495000000", - "last_update": 1710705189, - "custom_records": {} - }, - "custom_records": {}, - "node1_pub": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6", - "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1" + "pub_key": "0238268dc72b78f0bc19398b46916246723c013911286b91b61e7f78d7a9a90174", + "last_update": 1710602103, + "alias": "yangnamers", + "addresses": [ + { + "network": "tcp", + "addr": "ijnhxrqr2ekvbbh4qjtfjrganyerwtslldbnaav2xrp2y6bi2s3blrid.onion:9735" + } + ], + "color": "#023826", + "features": { + "1": { + "name": "data-loss-protect", + "is_required": false, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { + "name": "static-remote-key", + "is_required": false, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "last_update": 1710783641, + "alias": "DiamondHands\ud83d\udc8e\ud83d\ude4c", + "addresses": [ + { + "network": "tcp", + "addr": "62.77.157.54:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03ca2ce661ceb09c245588b8f4cb5dd041e387dca7d33ff3754eeb3928553aac98", + "last_update": 1710764530, + "alias": "unatco2", + "addresses": [ + { + "network": "tcp", + "addr": "p2s2w5go4h4xvr644m67kjfatkckw6u2yabpigahnqo2fvolcwmo7jyd.onion:9735" + } + ], + "color": "#f130f9", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "last_update": 1710842679, + "alias": "Btc-Tide", + "addresses": [ + { + "network": "tcp", + "addr": "79.7.125.218:9735" + }, + { + "network": "tcp", + "addr": "jh52l3y7waf2f7djheq7fahrjlsveisxxumgu3iznofxn4eqdyi3ggid.onion:9735" + } + ], + "color": "#5f1be8", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "029a0d058126445a179586a104a8425bc996b88b0bfc533eed34f49d34b1c3b684", + "last_update": 1710633903, + "alias": "Skaloshi", + "addresses": [ + { + "network": "tcp", + "addr": "vv3smsw3cuvsjmkzv7bh45etuialxjjspwnt3b25qfswym4btdztmayd.onion:9735" + } + ], + "color": "#15b331", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "last_update": 1710754265, + "alias": "satsophone", + "addresses": [ + { + "network": "tcp", + "addr": "uagt7hb6nw2dkv3gy2madsj4uu3xlpe47en7gtr7pilrvcse4j4pc4yd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "021aa6f0fdc8ea3c85b98aff27e5ffe1f67921e75e7aa7bad6a36d6085d54117df", + "last_update": 1710620720, + "alias": "021aa6f0fdc8ea3c85b9", + "addresses": [ + { + "network": "tcp", + "addr": "167.172.139.109:9735" + }, + { + "network": "tcp", + "addr": "iqitu5tyy2k4wljm67n4rwcjlriruzlsibc2llgn227stoy5jjf7o4ad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "022b0de0e9156024d3f1ba54b3602f21eae93c4087a552140d25805175d3d9a1d3", + "last_update": 1710854247, + "alias": "ninjasats440", + "addresses": [ + { + "network": "tcp", + "addr": "jd2w2yf7l76dbumpyvhk7ghc3soyoqfnzvsrnzcpf46vjjwqmybzinad.onion:9735" + } + ], + "color": "#f733ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "last_update": 1710848135, + "alias": "YTMND", + "addresses": [ + { + "network": "tcp", + "addr": "216.18.188.159:9735" + }, + { + "network": "tcp", + "addr": "[2001:18b8:0:100:0:b00b:420:69]:9735" + }, + { + "network": "tcp", + "addr": "jhyw24mdotsjpatyzmo23563n7fn26hjopavv6bmpn3xjta36bdadoyd.onion:9735" + } + ], + "color": "#ff7541", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "last_update": 1710856655, + "alias": "Megalithic.me", + "addresses": [ + { + "network": "tcp", + "addr": "164.92.106.32:9735" + }, + { + "network": "tcp", + "addr": "m563h7c5fdzq63d4znhkl2pbhskbf4meeybjgxmp7mkkp5j7osadgwad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "last_update": 1710851620, + "alias": "glr.com.py", + "addresses": [ + { + "network": "tcp", + "addr": "181.126.37.65:9736" + }, + { + "network": "tcp", + "addr": "qt3gn3lzwq5fe5obgo3y7ey3twiq43b7ifb57k666vulp2whe54rboqd.onion:9735" + } + ], + "color": "#5f04c0", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "last_update": 1710149277, + "alias": "Gondolin \ud83c\udfd4\ud83c\udfef\ud83c\udfd4", + "addresses": [ + { + "network": "tcp", + "addr": "ksdbl2gqtzcrkluuo2donqfifrdj5fqsl7ikyxjxbqta5k3my34ev5yd.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "last_update": 1710459313, + "alias": "gameb_2", + "addresses": [ + { + "network": "tcp", + "addr": "3.234.251.85:9735" + } + ], + "color": "#ffdc00", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "last_update": 1710811714, + "alias": "Ludwig", + "addresses": [ + { + "network": "tcp", + "addr": "170.75.171.98:9735" + }, + { + "network": "tcp", + "addr": "5fcvqllrm6szaiyxznlkto3qzoj56xeactxowiq5our3ge5zj366xjyd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "last_update": 1710666852, + "alias": "MindlinerTre", + "addresses": [ + { + "network": "tcp", + "addr": "62.12.168.100:9735" + } + ], + "color": "#fec89a", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "6": { + "name": "gossip-queries", + "is_required": true, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "25": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "29": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "35": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "39": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "41": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "43": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "105": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "163": { + "name": "unknown", + "is_required": false, + "is_known": false + } + }, + "custom_records": { + "1": "029a00320064000000024c4b40" + } + }, + { + "pub_key": "02b91642f44270c805e275ae59289f35d464de20cd3a546e3252710db6bab7c965", + "last_update": 1710840726, + "alias": "02b91642f44270c805e2", + "addresses": [ + { + "network": "tcp", + "addr": "zhnlchha3msn7um6yqztklrr7nmq2wh5w6mygbca55rhxjcxu3gbadyd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "last_update": 1710804164, + "alias": "i'llTryAnythingOnce", + "addresses": [ + { + "network": "tcp", + "addr": "23.88.103.101:40009" + }, + { + "network": "tcp", + "addr": "buqjxr6ha4oob7knppq2ky4urrtohruowpdpldjdapgn2k5gt2nx74id.onion:9735" + }, + { + "network": "tcp", + "addr": "b2abr4r5xjkxy32yj3eclnggyrywarau3cbc5tjshogqaopbdoywrayd.onion:9735" + } + ], + "color": "#d7cfad", + "features": { + "1": { + "name": "data-loss-protect", + "is_required": false, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { + "name": "static-remote-key", + "is_required": false, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "25": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "35": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "163": { + "name": "unknown", + "is_required": false, + "is_known": false + } + }, + "custom_records": { + "1": "029a0078000a000005dc03e8" + } + }, + { + "pub_key": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "last_update": 1710671588, + "alias": "Lightning.Watch", + "addresses": [ + { + "network": "tcp", + "addr": "[2a01:4f9:2a:1787:0:999:0:2095]:8888" + }, + { + "network": "tcp", + "addr": "95.216.23.120:8888" + }, + { + "network": "tcp", + "addr": "4gjavv3jm3ftzhtpprygss3y3yajxu2m6zx3isrlgb3irk3mzgtluwqd.onion:9735" + } + ], + "color": "#00fff8", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "last_update": 1710820471, + "alias": "southxchange.com", + "addresses": [ + { + "network": "tcp", + "addr": "54.187.0.23:9735" + } + ], + "color": "#428bca", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "035430a842187f9d27d639b05151f2100fb3e8370a0375e3c1c0ca8257cbe054d4", + "last_update": 1709325261, + "alias": "btc21m", + "addresses": [ + { + "network": "tcp", + "addr": "fftz6ctxdnd2io7bhi3yodx2onuzf7t5teh5rvqzmfdggskxybx3kkqd.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0215a106e71380f6b273e8232915f5fb3942822b1f8728d8d3ab8c9dc6eb2e8b02", + "last_update": 1710638255, + "alias": "0215a106e71380f6b273", + "addresses": [ + { + "network": "tcp", + "addr": "bed7wg2ujvvapyzlxd64ou2jekuclworvdei6fnc5axojdhu4evz3qad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "02b4f019af3b3fa23681c4cfd81f3031e100d2a2d395f0c9e7924de0a6f9f1079f", + "last_update": 1710839604, + "alias": "02b4f019af3b3fa23681", + "addresses": [ + { + "network": "tcp", + "addr": "4eizywhndgugjt7ki3wkc4lgjtrg5opzcvq5eoqe2yau7ro5n7yn5pid.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "last_update": 1710856659, + "alias": "lnd.measite.de", + "addresses": [ + { + "network": "tcp", + "addr": "84.172.88.215:9735" + }, + { + "network": "tcp", + "addr": "lmbxbzc4vj565r7eryrwvs52jq5vohyk5r6uqoceeebxhcs737tjr5yd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "last_update": 1710544303, + "alias": "JK-2", + "addresses": [ + { + "network": "tcp", + "addr": "ag6t5wvmumibm55dk5kh7spclqv54hy6zykyn4o6hvmx6c3mg2cmm4id.onion:9735" + } + ], + "color": "#9a34ad", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03a21a8756ffc55dd1d83a02b0cc2e163f8d041e3f56b309232e5389c66c00364c", + "last_update": 1710750973, + "alias": "Fulcrum", + "addresses": [ + { + "network": "tcp", + "addr": "54.70.181.161:9735" + }, + { + "network": "tcp", + "addr": "xp6nnobrvd2e7ojthcjadkfmansjndiu5t6dtfly6jg3vqf3nqbbwiyd.onion:9735" + } + ], + "color": "#ff5000", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "02d8853c04b69ce55c93d574bbb614e31f2654fb2f4049483774792cfd8d9af732", + "last_update": 1710824156, + "alias": "Zorbito", + "addresses": [ + { + "network": "tcp", + "addr": "jplka734tcxesjfmqzdacati4o5amnjapotobcrvkhriwog2fb43ggad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "last_update": 1710657422, + "alias": "KryptoKragon", + "addresses": [ + { + "network": "tcp", + "addr": "hs2zrv6qidyx5uptdw5jhouhc2xt6vwzregjir7fechc4a35dazxkwyd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "last_update": 1709327134, + "alias": "FREE PEERSWAPS", + "addresses": [ + { + "network": "tcp", + "addr": "140.82.21.181:9735" + } + ], + "color": "#03a465", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "6": { + "name": "gossip-queries", + "is_required": true, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "25": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "35": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "39": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "41": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "43": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "163": { + "name": "unknown", + "is_required": false, + "is_known": false + } + }, + "custom_records": {} + }, + { + "pub_key": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "last_update": 1710854344, + "alias": "sparksNode \ud83d\udca5", + "addresses": [ + { + "network": "tcp", + "addr": "w7ytfe7tms7ph7l6usancypopci254ztmlaqqwatpcu2pxjzxaj673id.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03d4809d1a129228b9afb54dd5b3bcc29b8e59d5c8fc1f3104315baf19c8e0c76b", + "last_update": 1710774723, + "alias": "Aerarium", + "addresses": [ + { + "network": "tcp", + "addr": "2svin2f6lssximwg53ydukjv33vix6mhlijqe5w5njodyuuby2s562ad.onion:9735" + } + ], + "color": "#f2a900", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "029ed7e456a53568e12f013173dc5be2a92c86302e15038d150362933b754e5d21", + "last_update": 1710749524, + "alias": "PaymentHub", + "addresses": [ + { + "network": "tcp", + "addr": "mbdkjteft7mej56utlstw4hr34br7lwy2mq7bowqxjy66vqrwwkbmkid.onion:9735" + } + ], + "color": "#e11a1a", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "last_update": 1710856043, + "alias": "LNB\u03dfG [Hub-1]", + "addresses": [ + { + "network": "tcp", + "addr": "213.174.156.66:9735" + }, + { + "network": "tcp", + "addr": "[2a02:b48:207:2:9735:9735:0:1]:9735" + }, + { + "network": "tcp", + "addr": "qimt6abvc2iuexwrtl5tzyrygnu7mshjahvresve5hdli6nstdg7elyd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "181": { + "name": "simple-taproot-chans-x", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "last_update": 1710736050, + "alias": "Moon (paywithmoon.com)", + "addresses": [ + { + "network": "tcp", + "addr": "52.86.210.65:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03fb1e396cc523f9823458197150b74fdc79b0478ee00f67f526cd1e14e1ad267d", + "last_update": 1710822085, + "alias": "QuantX", + "addresses": [ + { + "network": "tcp", + "addr": "45gmsvweqmvnms7w4jznv562ghnaubp7xm5c4surhnxmrekw6vlya5ad.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "027d4f8fd79e846b19c36891004c4e1c02e4ec6452112dd73a5761c971cf880ee5", + "last_update": 1710623633, + "alias": "Arolla", + "addresses": [ + { + "network": "tcp", + "addr": "izpqo6qaiqu6ap4t6ylvo32bykms3li5tly3f26dkxeny5vgsyleikid.onion:9735" + } + ], + "color": "#4c0099", + "features": { + "1": { + "name": "data-loss-protect", + "is_required": false, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { + "name": "static-remote-key", + "is_required": false, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "last_update": 1710856651, + "alias": "Friendspool\u26a1\ud83c\udf7b", + "addresses": [ + { + "network": "tcp", + "addr": "179.225.221.69:9735" + }, + { + "network": "tcp", + "addr": "uyf6gnzsf6nh5abwgktxdry3xcknigyzeulf4qiz5yn7mhzwhtppriqd.onion:9735" + } + ], + "color": "#15ae4d", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "last_update": 1710855943, + "alias": "\ud83e\udd81Bayer\ud83e\udd81", + "addresses": [ + { + "network": "tcp", + "addr": "2anl2m7xjazcgxskypq3upixmytqph4j7uhy5bx4y5up3kzdfh64zwqd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0268fb5ff483d584b81832b025b8ed122596d4b642171d71ab8b5893aa24eccece", + "last_update": 1710654812, + "alias": "Crypto Lover Couple", + "addresses": [ + { + "network": "tcp", + "addr": "46.101.122.181:29041" + }, + { + "network": "tcp", + "addr": "36zm4aba2z77zzolft6uscugroxdfkvgbbpjoqj2grtx3avaxu4dmoqd.onion:9735" + } + ], + "color": "#00ff41", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "last_update": 1710821651, + "alias": "OpenNode.com", + "addresses": [ + { + "network": "tcp", + "addr": "18.222.70.85:9735" + } + ], + "color": "#000000", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "last_update": 1710855272, + "alias": "SCP-096", + "addresses": [ + { + "network": "tcp", + "addr": "nnl5wxs3bjlm7yrqbmw4y3wjsas2mr7xw7km3gwvascgti6sbltbfpyd.onion:9735" + } + ], + "color": "#096096", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "028dcb644197ce997574bd24360df94d3f5cd7926caae51eded371e2926979ed19", + "last_update": 1710825844, + "alias": "bitcoin01", + "addresses": [ + { + "network": "tcp", + "addr": "5.75.184.195:14974" + }, + { + "network": "tcp", + "addr": "yiqo24vutzl62g7zvy2fzktquiipcstx4p6pzih5ufi7ds3yzhze5uid.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "0303e0423fd2c40d73978b03069f63d6ca5ae1abf61f5666bbbea886181f21e8e3", + "last_update": 1710855551, + "alias": "Node Up or Shut Up II", + "addresses": [ + { + "network": "tcp", + "addr": "159.223.176.115:21845" + }, + { + "network": "tcp", + "addr": "fhhz77ifwvymzeq4gzrjjsus74a6zdstv224lae5qyqlgsjthub3aiqd.onion:9735" + } + ], + "color": "#2199ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "last_update": 1710855402, + "alias": "lndus0.zaphq.io", + "addresses": [ + { + "network": "tcp", + "addr": "34.138.228.220:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "last_update": 1710856653, + "alias": "BitcoinVoucherBot-X", + "addresses": [ + { + "network": "tcp", + "addr": "ks545foo7bl57qldqqkh3alshu27dtkocqwp35zm4i5iblophgwm3vyd.onion:9735" + } + ], + "color": "#ff5000", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "024efd67cb4e01cdcd79e86f561cb5e66faea6d3761b5f33daca6afce02a80a7df", + "last_update": 1710181560, + "alias": "GG", + "addresses": [ + { + "network": "tcp", + "addr": "jwesmeaxdm4ydj5s46waffpwekiwcbvspos3snhtjg3nxqqiugs7joyd.onion:9735" + } + ], + "color": "#b00b69", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "181": { + "name": "simple-taproot-chans-x", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "030066a41ce86e73e96ffcbc3cf445744d5c92144e0c254c421a0204aebbdbb2bc", + "last_update": 1665576046, + "alias": "030066a41ce86e73e96f", + "addresses": [ + { + "network": "tcp", + "addr": "n7n4dimdw7ikvdcl5lp7xwx3rtxthi5hikxoqlxcpeujlhjjmehp4pad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "last_update": 1710804163, + "alias": "tippin.me", + "addresses": [ + { + "network": "tcp", + "addr": "13.113.39.53:9735" + } + ], + "color": "#ff4681", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + }, + { + "pub_key": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "last_update": 1710723270, + "alias": "Blockstream Store", + "addresses": [ + { + "network": "tcp", + "addr": "35.232.170.67:9735" + }, + { + "network": "tcp", + "addr": "giexynrrloc2fewstcybenljdksidtglfydecbellzkl63din6w73eid.onion:9735" + } + ], + "color": "#02df5f", + "features": { + "1": { + "name": "data-loss-protect", + "is_required": false, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { + "name": "static-remote-key", + "is_required": false, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "25": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + } + }, + "custom_records": {} + } + ], + "edges": [ + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "620141050860732416", + "chan_point": "51f9e44982900447e0a1974a8750c4505962f9619aaff8dade1ef3f9847ad1d3:0", + "last_update": 1710827563, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710785048, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710827563, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "688771467187060737", + "chan_point": "b1291dec49d7c853927b6c9cc0080a2bdd1794e43d397ac82c1910faed89e8ef:1", + "last_update": 1710811629, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710811629, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 30, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1700", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1682692390, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "channel_id": "701149769035481088", + "chan_point": "a0bae4458e28736b64c0335d7f2108b6ee8c1070ddf09949c2fd1b5b27a90c29:0", + "last_update": 1710844897, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "500000000", + "last_update": 1710844897, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "400000000", + "last_update": 1710806238, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "701231132948234240", + "chan_point": "f65a27eed21a5d4ee23466e7ba6960971c8f85ec3c2d19edf99cbb018a785401:0", + "last_update": 1710855697, + "capacity": "800000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "20", + "disabled": false, + "max_htlc_msat": "792000000", + "last_update": 1710855697, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "792000000", + "last_update": 1710715963, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "channel_id": "701275113349120000", + "chan_point": "b6fb3aee05bffaa82bdaa6b5197c971beced63c27e371a6e888f417df0336787:0", + "last_update": 1710769297, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710769297, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 30, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1700", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710541727, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "715544575268356096", + "chan_point": "3108d456a2138600b302fc82be2dda3b1aea982a49ffcadeb6c25fdbb34aa6ac:0", + "last_update": 1710839931, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710839931, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710773829, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035430a842187f9d27d639b05151f2100fb3e8370a0375e3c1c0ca8257cbe054d4", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "721567699888766976", + "chan_point": "33664578a67d720dd169aeb3a77928c7176a1fd385f84f10e768fe9e6d8d9ca9:0", + "last_update": 1710832963, + "capacity": "800000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "792000000", + "last_update": 1709162031, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": true, + "max_htlc_msat": "792000000", + "last_update": 1710832963, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "channel_id": "722185625535643649", + "chan_point": "f67e8d32b71600f0d2f73dde57215a4c38911e23cf8d0c0c1be55647fe52bf80:1", + "last_update": 1710817031, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "85", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710772420, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710817031, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "node2_pub": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "channel_id": "722285681141153792", + "chan_point": "722aca928ff65b4ce522972bbb06ff3663bff398f970f28bea886a5800e0832f:0", + "last_update": 1710808897, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "85", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710808897, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "85", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710714820, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "723662269581033472", + "chan_point": "ff2ea32c7db8d11d34e06235732449810ca3e1fa19efeea45375a11652904bc8:0", + "last_update": 1710786481, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "85", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710729220, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710786481, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "0238268dc72b78f0bc19398b46916246723c013911286b91b61e7f78d7a9a90174", + "channel_id": "739309419534417920", + "chan_point": "5f4e8328eb811a5d19ad685fa8e21373dcb2495873aea913c8ebab0b98b5d0d8:0", + "last_update": 1710822429, + "capacity": "4007237", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "7997", + "disabled": false, + "max_htlc_msat": "3967165000", + "last_update": 1710256181, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4007237000", + "last_update": 1710822429, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "node2_pub": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "channel_id": "744180256119783424", + "chan_point": "b74b95fceca412ebecf4bd672197af3be7c684f0e73c2af8574a350bac496d48:0", + "last_update": 1710855697, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710855697, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710540707, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b4f019af3b3fa23681c4cfd81f3031e100d2a2d395f0c9e7924de0a6f9f1079f", + "node2_pub": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "channel_id": "744329789687267328", + "chan_point": "fd22f30704401a9445dc98642e28867f28d1fae4c99197db15e79898bcf65883:0", + "last_update": 1710826897, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710804724, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710826897, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "node2_pub": "035430a842187f9d27d639b05151f2100fb3e8370a0375e3c1c0ca8257cbe054d4", + "channel_id": "744640951516135424", + "chan_point": "a77c6ee8630e93062245f2a2bdbb687b7d6e92e63880d49dd23853ec1db44ad6:0", + "last_update": 1710536713, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1709328274, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": true, + "max_htlc_msat": "99000000", + "last_update": 1710536713, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032261d6ec5fb578919e2455f2061d27bd93df57b26d78cdf7bad38f1368d0531b", + "node2_pub": "0268fb5ff483d584b81832b025b8ed122596d4b642171d71ab8b5893aa24eccece", + "channel_id": "746389174898458624", + "chan_point": "e5f14c45f306e9969d1d43a68582540e41c506174d7f49cd00c5375c4f038067:0", + "last_update": 1710779200, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710779200, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "9", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710773240, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "749958189634486272", + "chan_point": "dce410d1f7f7bb1ff7fc14422e94859097c7ce9760b3e55198e85518318b9265:0", + "last_update": 1710852943, + "capacity": "12309789", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "12186692000", + "last_update": 1710852943, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "12186692000", + "last_update": 1710842221, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "node2_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "channel_id": "750026359397154817", + "chan_point": "a594d8ec35606a44907f73d5e55cefedfca340f342ed1d6d9d6f7ff9c30277e0:1", + "last_update": 1710810322, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710810322, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710750913, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "022ea68ac7588959a6a4829a5ff176c296329d614b90b3f5f4456b5d6130ea50f6", + "channel_id": "756620130651340800", + "chan_point": "411123b7fea933ddb7c8d7160ee0fd6605fbda7e0a846124b02c74468b9db635:0", + "last_update": 1710817552, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1250", + "fee_rate_milli_msat": "155", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710817552, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "935", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710802531, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "758001117307863040", + "chan_point": "c7b275977af7a733df5770f5198a71aca908636350aa4ce9bd652c958d150adc:0", + "last_update": 1710842131, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710786343, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1235", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710842131, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "channel_id": "758288089872334849", + "chan_point": "4a6286a1ed3704c87127ab09e4c02b271708692277822b53228c7f53a37460e2:1", + "last_update": 1710842620, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "4000", + "fee_rate_milli_msat": "70", + "disabled": false, + "max_htlc_msat": "4000000000", + "last_update": 1710842620, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710560636, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "node2_pub": "02b4f019af3b3fa23681c4cfd81f3031e100d2a2d395f0c9e7924de0a6f9f1079f", + "channel_id": "758424429205323776", + "chan_point": "3591fa9a442c632e3ad1e3ec237a2a2dc1e4863e8bef1bf7b4d96d4775984f08:0", + "last_update": 1710856924, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710856924, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "200", + "fee_rate_milli_msat": "85", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710779620, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "channel_id": "758660824200314880", + "chan_point": "dbc44c5f794126476de6dea961563903fb73d343127a969b441f36ee8970f262:0", + "last_update": 1710771931, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710651913, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "895", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710771931, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "022ea68ac7588959a6a4829a5ff176c296329d614b90b3f5f4456b5d6130ea50f6", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "759079738155204608", + "chan_point": "596431238183ba525a8d51a2cceda27aa918cbc3dde56da36f9af7867094502c:0", + "last_update": 1710817552, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "228", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710817552, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710786481, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "channel_id": "760095686837338113", + "chan_point": "e71273fda83b976b718b6ae1e0fd345efcdf0372b328e6c2ee51093724284376:1", + "last_update": 1710806734, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "198", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710806734, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710795430, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "761152317513072640", + "chan_point": "292765512abde4f0bea1ab8b99ff50cfb783a4115521165a39d8c1238b3d6266:0", + "last_update": 1710584731, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710561936, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3985", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710584731, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "channel_id": "764089113104547840", + "chan_point": "b02a5d74c012604c1a39b5b6bbc79f992eed3b37ff22badeb4b76c39e939dff2:0", + "last_update": 1710828220, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "85", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710828220, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "13000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "715", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710631531, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "02b4f019af3b3fa23681c4cfd81f3031e100d2a2d395f0c9e7924de0a6f9f1079f", + "channel_id": "764533315781001216", + "chan_point": "4c1c1da13e6101b9fb9b2df46d612f7e12994ec91521f455f20641f35736fd4e:0", + "last_update": 1710781948, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710781948, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "9000", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710777724, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "03ff4d7532fc749c731de8bdac7226c06fdb82a458cd7ee557a6e355f5b359fd84", + "channel_id": "766235359892471809", + "chan_point": "354971270acb1ee8720e098b8fc9c2a3dd7b15b9e8699f7f5cd842e62e51b1cc:1", + "last_update": 1710708531, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "990000000", + "last_update": 1710708531, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1663586903, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "channel_id": "769044612082958336", + "chan_point": "245cf3989871e89fe2ddc7920745e128483a700740b7ca41121dcab359377901:0", + "last_update": 1710781450, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710781450, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1235", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710753931, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0268fb5ff483d584b81832b025b8ed122596d4b642171d71ab8b5893aa24eccece", + "node2_pub": "024efd67cb4e01cdcd79e86f561cb5e66faea6d3761b5f33daca6afce02a80a7df", + "channel_id": "772235394777808896", + "chan_point": "a6d336e928d17adee83056a7d197535af1726cf3472d3186ca8d274040cd923e:0", + "last_update": 1710796663, + "capacity": "1050000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "1033507000", + "last_update": 1710439360, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "1039500000", + "last_update": 1710796663, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "772480585848061953", + "chan_point": "ba90c6dab9a8c1566510af082dfaa0f9fc9b52f08f3bf154a1b804006ff62389:1", + "last_update": 1710852935, + "capacity": "89000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "88110000000", + "last_update": 1710836821, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "88110000000", + "last_update": 1710852935, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03fb1e396cc523f9823458197150b74fdc79b0478ee00f67f526cd1e14e1ad267d", + "channel_id": "773543813641273345", + "chan_point": "0e82cab15a220e7ccee1e1768a24ab0f70599531f6f70a9c9e06bb4934cc2f4a:1", + "last_update": 1710836328, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710752454, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "8", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710836328, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ca2ce661ceb09c245588b8f4cb5dd041e387dca7d33ff3754eeb3928553aac98", + "node2_pub": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "channel_id": "773691148107710465", + "chan_point": "bf279306711674bc1912dee89c39bf5ae856a6cb93c00d0e0878eefed366e1fb:1", + "last_update": 1710120872, + "capacity": "800000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "792000000", + "last_update": 1706625249, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "340", + "disabled": false, + "max_htlc_msat": "800000000", + "last_update": 1710120872, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "774886317251231745", + "chan_point": "99a561caf4ca8475fcdd8b53a2ea0e7c60cba61a204dfd5292f5a13e187668ef:1", + "last_update": 1710610081, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "640", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1709929677, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": true, + "max_htlc_msat": "16609443000", + "last_update": 1710610081, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "775701055451299841", + "chan_point": "d166a84cdd3d7808ce364e1e546c11f699838d88ee10c21ef26cbea63815acff:1", + "last_update": 1710849420, + "capacity": "9242005", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1200", + "disabled": false, + "max_htlc_msat": "9149585000", + "last_update": 1710813366, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": true, + "max_htlc_msat": "9149585000", + "last_update": 1710849420, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "node2_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "channel_id": "780701634360573953", + "chan_point": "788f904a9ac3f959c608bd6bafd6db79674ac080469e73d8968799836e8d2ae6:1", + "last_update": 1710806564, + "capacity": "2501000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "2475990000", + "last_update": 1710806564, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "480", + "disabled": false, + "max_htlc_msat": "2475990000", + "last_update": 1710038727, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "781361341298442241", + "chan_point": "ebd7caf7f989304f073345dd5724946b950f94bbc1dbef1974ee1146d6d23fa7:1", + "last_update": 1710780040, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710779135, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710780040, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "782331110584549377", + "chan_point": "bf2255817b69dc1ab29b89ad9dc4c076a306466d8061364695fc371ab23e501e:1", + "last_update": 1710720579, + "capacity": "18000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "17820000000", + "last_update": 1710720579, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "17820000000", + "last_update": 1710720579, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "783190928622419969", + "chan_point": "61633c6615cb1eb0a6972058258565a64d5c3f9a73464b11b3b4bfa49e17a014:1", + "last_update": 1710772997, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710757535, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710772997, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "node2_pub": "032261d6ec5fb578919e2455f2061d27bd93df57b26d78cdf7bad38f1368d0531b", + "channel_id": "784059542792896515", + "chan_point": "44fee5d5755730f18c7e83062a5dbd2e71c70d4810848d8a345c2174e3c6e0cc:3", + "last_update": 1710779065, + "capacity": "5000001", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "24", + "disabled": false, + "max_htlc_msat": "4950001000", + "last_update": 1710773240, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950001000", + "last_update": 1710779065, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "784409187488563201", + "chan_point": "59810083dd5f98adf1d5772c1e33b033d83429ece66c9d2ddef94b8832b1dbc3:1", + "last_update": 1710806734, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "197", + "disabled": false, + "max_htlc_msat": "4000000000", + "last_update": 1710806734, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710794122, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "784923758971518977", + "chan_point": "cfd4b66a48df7e6e59ef255110b3f9e888b1dd82ed576fc67bb1d5591c9c86b2:1", + "last_update": 1710795421, + "capacity": "4500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "4455000000", + "last_update": 1710795421, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "197", + "disabled": false, + "max_htlc_msat": "4000000000", + "last_update": 1710794134, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "channel_id": "785205233929355265", + "chan_point": "cf1960c00d14fe723cf0255176a9bde30411789c30386ebf61f399af46bc14bc:1", + "last_update": 1710655534, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "97", + "disabled": false, + "max_htlc_msat": "3600000000", + "last_update": 1710655534, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": true, + "max_htlc_msat": "3960000000", + "last_update": 1710609480, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "channel_id": "785553779142426629", + "chan_point": "0cede6d3919f2e6bc4eb84811b3afe4d6e2c252843d1880708d97e9dfd35aff7:5", + "last_update": 1710855660, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "16", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710855660, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710785354, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "789199759664087041", + "chan_point": "350139d94a9922a5b6da9e38617efbe8525fb23330df1e06b3d3c324561a5f6d:1", + "last_update": 1710838531, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710837054, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1975", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710838531, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "node2_pub": "0268fb5ff483d584b81832b025b8ed122596d4b642171d71ab8b5893aa24eccece", + "channel_id": "789307511875502081", + "chan_point": "ce5998eb75e3442113c4f826ac241132534a02dc6e044a9fa96f3bd8ae6cc906:1", + "last_update": 1710811121, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710811121, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710788713, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "channel_id": "789538409204154370", + "chan_point": "47c306bb98b25779f0c09076c3920da5a2f0026a2c2e10baecd105b68834634d:2", + "last_update": 1710818239, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "91", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710818239, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1709312781, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "channel_id": "789757212095741952", + "chan_point": "65a50b8b2c6d4a7927c9fb21cd6d2d40fd31934f096f72cc751db053e2b04d2c:0", + "last_update": 1710839400, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "99", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710839400, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710752120, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "channel_id": "790669806689648640", + "chan_point": "82ee77f42f481281c6d44f1f3b8f1f1c33355cd2817e25ec801bd2ff70c1431c:0", + "last_update": 1710609931, + "capacity": "6000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "64", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1709915277, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "915", + "disabled": true, + "max_htlc_msat": "5940000000", + "last_update": 1710609931, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "791507634547523585", + "chan_point": "33c4d97d7baf5e5eb1932e89225a812e33eb774245596e3f921eee0d86404365:1", + "last_update": 1710810322, + "capacity": "7000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "75", + "disabled": false, + "max_htlc_msat": "6930000000", + "last_update": 1710810322, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1635", + "disabled": false, + "max_htlc_msat": "6930000000", + "last_update": 1710809731, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "791756124250832900", + "chan_point": "ee1558769eb0eca1f6c5a1767bf3a8907b656a4b304a2e2df9ed462a7fd73c4c:4", + "last_update": 1710773731, + "capacity": "2653177", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "2626646000", + "last_update": 1710681712, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1385", + "disabled": false, + "max_htlc_msat": "2653177000", + "last_update": 1710773731, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "791977126006685697", + "chan_point": "2a95a80acade5c17e48d9d459e71dd53ec5ccd4d8f097ebd4d388c1a9ca3f626:1", + "last_update": 1710796240, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710796240, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "50000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "5445000000", + "last_update": 1710780607, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "792265198100545538", + "chan_point": "0213392bcbab4832e1407f89db7840290f46eb0d13b4d331bb1076ca762d9d63:2", + "last_update": 1710772081, + "capacity": "2420637", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "2420637000", + "last_update": 1710772081, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "2396431000", + "last_update": 1710681712, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "792920507072315401", + "chan_point": "65263a90b7a4540889cddc01e52394a8fb7e40ea20b31ce8014c2f9408f198b1:9", + "last_update": 1710804913, + "capacity": "5783984", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "5726145000", + "last_update": 1710681712, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": false, + "max_htlc_msat": "5783984000", + "last_update": 1710804913, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "channel_id": "793525238406971393", + "chan_point": "caadc0698b6056125abea9442a5eb176b1a8a618c167af61444003cb9699656c:1", + "last_update": 1710812440, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1708742048, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710812440, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "channel_id": "793978237322330113", + "chan_point": "33c4b8a2f9d6f9454ade27fa3b6eb1f87055e15f181145068da8b42d7775da2e:1", + "last_update": 1710786429, + "capacity": "12000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1536", + "disabled": false, + "max_htlc_msat": "11880000000", + "last_update": 1709945877, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": true, + "max_htlc_msat": "11880000000", + "last_update": 1710786429, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "channel_id": "794937011285655566", + "chan_point": "d89889cb7682308568524adb82717c87752ecafc7a00ecf5466c6def71762b12:14", + "last_update": 1710714007, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": true, + "max_htlc_msat": "4950000000", + "last_update": 1710614040, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "50000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "2722500000", + "last_update": 1710714007, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "795094241517633537", + "chan_point": "076eadbbef31ce6fc3a220c7710ac7dd0e2decc2a44760db6c38c89f19295d35:1", + "last_update": 1710814009, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "114", + "disabled": false, + "max_htlc_msat": "300000000", + "last_update": 1710814009, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1707761788, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "795647295890849793", + "chan_point": "f3e568ad6cc71f29d594ad8b140d86285eb2438ab37b95f60fb486c1a4913282:1", + "last_update": 1710827830, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "1000", + "last_update": 1710815204, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710827830, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "036dcba93cfc2d7294e586a658c90313d44fb742d832f0f79eb6e4361bd0afa707", + "node2_pub": "02b4f019af3b3fa23681c4cfd81f3031e100d2a2d395f0c9e7924de0a6f9f1079f", + "channel_id": "796153071199649794", + "chan_point": "202a07531f018476a31d6cb53d44defd5940f3339b3bc0aa87239ac70e71a464:2", + "last_update": 1710833524, + "capacity": "1108125", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": true, + "max_htlc_msat": "1108125000", + "last_update": 1710833524, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "4", + "disabled": false, + "max_htlc_msat": "1097044000", + "last_update": 1697545004, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "797418609082433536", + "chan_point": "f7243b067bf35ac4514abba7f03397c3a2eed1b4d7a59884da02e41d15731d26:0", + "last_update": 1710806734, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "198", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710806734, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710793681, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "797748462667366401", + "chan_point": "da6638149f2fd5008740e24d534973207868dddad4eaa5fa6e63e5f7079e3685:1", + "last_update": 1710827830, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710827830, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710810322, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "798970020045848578", + "chan_point": "eac6beb07894a0fa465f1eff2d7aa54123331bf6d2f45172d66005b7f18b7d16:2", + "last_update": 1710708676, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "9999", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1709637423, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": true, + "max_htlc_msat": "4455000000", + "last_update": 1710708676, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "799250395424751617", + "chan_point": "a061ad2ecc55e55ea2d822218cd520128347bf1141b1199dd0e9f52f49e1a0a9:1", + "last_update": 1710852931, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710836821, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1435", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710852931, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "799701195284021248", + "chan_point": "d2c2328c6966c3109d6f65dff475a8bcbcf52a34ba7ea33742768d352043bc3f:0", + "last_update": 1710847301, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710790869, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "17820000000", + "last_update": 1710847301, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "800123407672016896", + "chan_point": "9940e2aab0c5583f7a3499d09e769151f807040d1c3dcd758c5f8cba8337a05e:0", + "last_update": 1710796901, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "17820000000", + "last_update": 1710796901, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710790503, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "800921653136850946", + "chan_point": "a8fd2db7a9fe5808c3e2ef823a6cb3d32e0489d12d49662271a78645cd6e5c07:2", + "last_update": 1710702089, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "250000000", + "last_update": 1710702089, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710681712, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "801269098805985284", + "chan_point": "b49f6e0e627b9d818ce6b2e44694221e130d4d6e04b30f3a8a74120bd5f8ec05:4", + "last_update": 1710796901, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710790875, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "17820000000", + "last_update": 1710796901, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "022ea68ac7588959a6a4829a5ff176c296329d614b90b3f5f4456b5d6130ea50f6", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "801355960168546304", + "chan_point": "ee3ee6f2e532c6d79fe9af90e4ca39fc6cc66fbc14208ec272f8d387c35f3365:0", + "last_update": 1710786952, + "capacity": "20000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "19800000", + "last_update": 1710786952, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "19800000", + "last_update": 1710717263, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "802335625163309057", + "chan_point": "7eda2bb26517138b63413014089750f438bb70dd7e985f6c37c154897f9c53e8:1", + "last_update": 1710822553, + "capacity": "8100000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "8019000000", + "last_update": 1710822553, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "8100000000", + "last_update": 1710814347, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "802866689216413699", + "chan_point": "0d4441935389214c0842df3d726bc9815f204bff89d5163717d721a764ebcee3:3", + "last_update": 1710791593, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710791593, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "998", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "17820000000", + "last_update": 1710791405, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "803464823554441216", + "chan_point": "af5690e2b228bff347e81d03deefb5ba1702fed8cf9438431a5ba577d54f5c24:0", + "last_update": 1710807040, + "capacity": "10100000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9999000000", + "last_update": 1709912076, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "980000000", + "last_update": 1710807040, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "803464823558176768", + "chan_point": "4ddde3c9c96fc04cc5a0b54fa979f6eee573c3c62bab2c13f3b600dad1e544a4:0", + "last_update": 1710801358, + "capacity": "6000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "799", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710801358, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "71", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710729322, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "803830960949624833", + "chan_point": "2eaf9f68f25532373b0bd0b759f717ec5e7bc40635046dcc6b644b7d85e0a1fb:1", + "last_update": 1710825931, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710719754, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1975", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710825931, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02ab80cce7e7dff00c964e4b2006eb4c8af4e71187572156a88275d3c126b90b66", + "node2_pub": "03fb1e396cc523f9823458197150b74fdc79b0478ee00f67f526cd1e14e1ad267d", + "channel_id": "803837558058713088", + "chan_point": "e03e1e5242eeb60a2aa836aea1b13695f86a38c4e828892cb0e7d4072d72b530:0", + "last_update": 1710838129, + "capacity": "999888", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "989890000", + "last_update": 1707685141, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "989890000", + "last_update": 1710838129, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "805329595270955009", + "chan_point": "bd0f1a6ad4311bda84a000a0fce87d2a13ee5a77ba9748bcc51910bbdfe5bac3:1", + "last_update": 1710847531, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710791331, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "475", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710847531, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "805783693574995969", + "chan_point": "3bff0680981d6c2d3c677de40f607832c7cb23f7ba7f584f05b79319b2062a94:1", + "last_update": 1710794663, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710794663, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "735", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710788131, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "806033282704408580", + "chan_point": "b277272a004f49af3463a4644090bb83c9b8d91703fccdd3918a3f9542692706:4", + "last_update": 1710734649, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "107", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710734649, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "799", + "disabled": false, + "max_htlc_msat": "8000000000", + "last_update": 1710657542, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "channel_id": "806638014049681412", + "chan_point": "9db0a59700a59ac3084d832c2c207d4b188478212edc09591b0a37ec4be51b4c:4", + "last_update": 1710839839, + "capacity": "6000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "799", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710785158, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "94", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710839839, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "806738069744975873", + "chan_point": "e7e7789109224cebd2366587ca8092bbc0c346aaad8007599682660e2d63af3e:1", + "last_update": 1710849672, + "capacity": "18100000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "888", + "disabled": false, + "max_htlc_msat": "17919000000", + "last_update": 1710403944, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "18100000000", + "last_update": 1710849672, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "807119600233152513", + "chan_point": "7757d9e9fd758cb20972c70bec8dc391436a2ddfd4d84fdcf84c98f4b888340e:1", + "last_update": 1710806758, + "capacity": "6000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2199", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710806758, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1235", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710752129, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "807581395061833734", + "chan_point": "5f7b6de8466907c8c546787d032cb4fa53fee90d80e976a72712606d37ab675a:6", + "last_update": 1710854501, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710790852, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "17820000000", + "last_update": 1710854501, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "807616579452010497", + "chan_point": "e5d06a7863e10a94c821e1d06b36363ae9448233d62cf4e3beb70a4ef7ebf9c9:1", + "last_update": 1710655516, + "capacity": "10100000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "777", + "disabled": true, + "max_htlc_msat": "9999000000", + "last_update": 1710655516, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "41", + "disabled": false, + "max_htlc_msat": "10100000000", + "last_update": 1710635722, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "808307072735903745", + "chan_point": "d649d302b4d9c390b5105d37b87b3386d55123c161f8b6ddecc42b30007c80d1:1", + "last_update": 1710815204, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "2000000", + "last_update": 1710815204, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710752221, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "channel_id": "808473098955456512", + "chan_point": "8d9721d1be96c3f43efa06f1ae68b90e1dce339b9a0874d5d34e4dcd2106c834:0", + "last_update": 1710820522, + "capacity": "60000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "59400000000", + "last_update": 1710820522, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "59400000000", + "last_update": 1699766688, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "808896410953383937", + "chan_point": "720323d7116dd77a538b2f4c15ada94a7c3a8f7ad7cb4e32db8f63b852755d0b:1", + "last_update": 1710806131, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "9900000000", + "last_update": 1710655517, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2435", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710806131, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "808898609969102848", + "chan_point": "a723e097119278df2c3b0206c2d5ef13132dc943a7261ee3969c74b125b8e614:0", + "last_update": 1710800407, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "50000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "5445000000", + "last_update": 1710800407, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1235", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710797131, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "810368657035362304", + "chan_point": "1ad351dcf4c3b6ca2aaf36604924067b3f9bb75d8a5db769c6809a884dea904e:0", + "last_update": 1710845063, + "capacity": "20000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "19800000", + "last_update": 1710700997, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "19800000", + "last_update": 1710845063, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "811119623527333903", + "chan_point": "c5f317747bede72f3396da01c89223226cb7810bba15224d0d75c8939124fdd3:15", + "last_update": 1710851454, + "capacity": "60000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1799", + "disabled": false, + "max_htlc_msat": "59400000000", + "last_update": 1710752758, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "59400000000", + "last_update": 1710851454, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "813320845707902984", + "chan_point": "6e558eb7da9d2f5c36747a07dc9879f7daa3ff804ccdd66fd903fa7872de796b:8", + "last_update": 1710811039, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "297", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710811039, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710810322, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "channel_id": "814367580879912961", + "chan_point": "1124dc3acc435e21baaa8aaa7d6c2cbb29a6f079c9ea6dc42099abb70c5957ab:1", + "last_update": 1710845428, + "capacity": "1300000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "459", + "disabled": false, + "max_htlc_msat": "1300000000", + "last_update": 1710845428, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "400000000", + "last_update": 1710726786, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "channel_id": "814641359296331777", + "chan_point": "f2d4029d0996f11ca6647c02927ced47984202f5751ce87f1e32c8bbfa9db9a1:1", + "last_update": 1710849758, + "capacity": "1200000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "243", + "disabled": false, + "max_htlc_msat": "1188000000", + "last_update": 1710845430, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "1188000000", + "last_update": 1710849758, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "02d8853c04b69ce55c93d574bbb614e31f2654fb2f4049483774792cfd8d9af732", + "channel_id": "814898644921352193", + "chan_point": "35b562152952cac28125638260d881e8506aa32ee172b0fc207765b3ab90e772:1", + "last_update": 1710847556, + "capacity": "10096408", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9995444000", + "last_update": 1710847556, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "9995444000", + "last_update": 1710785048, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "channel_id": "815290071134175232", + "chan_point": "29953ec6177517ecdd5cfa11e4628f0a3f6d58192b9378d370f148485664b349:0", + "last_update": 1710790558, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1499", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710790558, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710784534, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "815498978337095681", + "chan_point": "e0d844875750d25b0d241035abcfc8f8afa180c12d7ecd2e028f16e32fad4e06:1", + "last_update": 1710827831, + "capacity": "119810484", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "118612380000", + "last_update": 1710827831, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "118612380000", + "last_update": 1710785048, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "815763960674189313", + "chan_point": "ea6b1b28400c8538a21f46b4dcc6c0c73581c49a3bf184483ad29e97c2057f7f:1", + "last_update": 1710836563, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710624431, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710836563, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "816879965000040450", + "chan_point": "eb8a7198857d38749735eee23e8eaf06f50412e708342ddd1e575f780a8b679f:2", + "last_update": 1710854901, + "capacity": "4900000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "55", + "disabled": false, + "max_htlc_msat": "4851000000", + "last_update": 1710854901, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "77", + "disabled": true, + "max_htlc_msat": "4900000000", + "last_update": 1710718926, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "817004209678909440", + "chan_point": "57cf522de8800edbf8256dc00576c13ce2b4d99e239395884ec16674c01e7814:0", + "last_update": 1710780632, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "672", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710780632, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710457811, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "817314272016531457", + "chan_point": "b81013d0591a53ec0f54eda76cf275443e1a1e064dcb25bcb80f28fd5b35729b:1", + "last_update": 1710856501, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "90", + "fee_rate_milli_msat": "9", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710856499, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710856501, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "channel_id": "818579809851408384", + "chan_point": "1f4e9fd59d7993bc888e0b2ee6d22b76e0f46f3d9184db144c3ea8ddc8d5fccf:0", + "last_update": 1710818407, + "capacity": "21000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "598", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710711542, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "50000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "15592500000", + "last_update": 1710818407, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "818992126750556161", + "chan_point": "6fb2f10d4d6edbeed8e407ff0353b383dc851b2bfd54f1b6c41ec7ee4395b5e5:1", + "last_update": 1710845431, + "capacity": "1100000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "63", + "disabled": false, + "max_htlc_msat": "1089000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "7", + "disabled": false, + "max_htlc_msat": "1089000000", + "last_update": 1710845431, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "node2_pub": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "channel_id": "819130665267560448", + "chan_point": "a4eb10554e71c53d65c48fcc7c7c0880aa27915d06c8e0137534d4256f960ee4:0", + "last_update": 1710681712, + "capacity": "13715301", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "0", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": true, + "max_htlc_msat": "13578148000", + "last_update": 1709514628, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "0", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "13578148000", + "last_update": 1710681712, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "819157053425451009", + "chan_point": "ff07d33e87d26d8dfaa7326e0b996a79954662c65d2dc3e0aab2ad8bd093b328:1", + "last_update": 1710854830, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1135", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710836574, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710854830, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "channel_id": "820151011993255937", + "chan_point": "e67db547e7aaf49d6350eb2d96edca4c2b9f6212b37cc059de8ff799fb27e557:1", + "last_update": 1710780171, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2199", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710780171, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710780171, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "channel_id": "820470969959645185", + "chan_point": "a4e2007f486a5c9d09b52422e9913c27d38e97495ae506ac328ba0b8ccd92d82:1", + "last_update": 1710819358, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1499", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710819358, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710785048, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "821415450426408960", + "chan_point": "957f40ccb45b4370ad8cc925f2e57f743a3d65224c9a785ad98f4d827955eb17:0", + "last_update": 1710806281, + "capacity": "12100000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1414", + "disabled": false, + "max_htlc_msat": "11979000000", + "last_update": 1710660964, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "12100000000", + "last_update": 1710806281, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "821637551705751553", + "chan_point": "34cc54265d19df14e9c34690a2423e1e1e1cc4b761f23135fe7e7419cb20f432:1", + "last_update": 1710822101, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "8910000000", + "last_update": 1710822101, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1975", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710790879, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "channel_id": "821934419824279552", + "chan_point": "743ef873e1279e8adcc6123cd6a9e2c382efdc22b9ddd1bc01b746f14599a396:0", + "last_update": 1710856511, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1500", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710780774, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710856511, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "node2_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "channel_id": "822279666624823297", + "chan_point": "05fd621b62f1eb2b9640b47e34ecab821817c71069ad01e658b4cc08b5826771:1", + "last_update": 1710783271, + "capacity": "1200000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "1188000000", + "last_update": 1710783271, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "480", + "disabled": false, + "max_htlc_msat": "1188000000", + "last_update": 1710163678, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "channel_id": "822307154289688576", + "chan_point": "75dcbd5fa4f22cc19ffff1b8b748ed1e338ccb5a375db7e4f38fb2b327c96845:0", + "last_update": 1710793854, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "198", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710752734, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710793854, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "823024035938631682", + "chan_point": "7e9a24c1269351ee781d6bfdcd799e187e2ceef9be0e9cb1316be6ad2c3222b2:2", + "last_update": 1710806230, + "capacity": "12100000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1750", + "disabled": true, + "max_htlc_msat": "11979000000", + "last_update": 1710655516, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "12100000000", + "last_update": 1710806230, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "823777201318133760", + "chan_point": "cd8fc05f50bf3069dfa14deb9bb2e7c15519aef5cd2527cf690fd28e51e3065d:0", + "last_update": 1710705189, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "751", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710528461, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "5000000000", + "last_update": 1710705189, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "824553456504930304", + "chan_point": "dffd6b183bca5502f7aab8598667bceb4832e2ddfd69d7fa2c93ac3532bf88a4:0", + "last_update": 1710849654, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710838110, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710849654, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "824721681831821313", + "chan_point": "c44c77cad6456eafb76f5f367fdba69d05448808af661d38a0f0bd3492443a80:1", + "last_update": 1710855641, + "capacity": "8171129", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "8089418000", + "last_update": 1710705633, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "170000000", + "last_update": 1710855641, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "827267051220697089", + "chan_point": "bb1b6b514adf1e3faede61de4bcffe7d2bd45528ef4dd40e5c99f0f752ea368f:1", + "last_update": 1710856490, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1500", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710856490, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1345", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710856489, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "827800314376945665", + "chan_point": "fb9a59e2481c92d8502af4939b50030779855fbe81250a826dbe1ff6e57dc133:1", + "last_update": 1710849335, + "capacity": "40000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "39600000000", + "last_update": 1710849335, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "299", + "disabled": false, + "max_htlc_msat": "39600000000", + "last_update": 1710785158, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "827920161201848320", + "chan_point": "5357e4bf6b9313b47d35369e7e54f63c9d226ae349d5d7fe44461d3e124e3ea0:0", + "last_update": 1710855358, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710849421, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710855358, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "827925658839678977", + "chan_point": "caea5a15c32c07087d71fa8277a237dd0bdf5c8f4f41f554e053eb6ef9c7d3c6:1", + "last_update": 1710825484, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "15", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "397", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710786958, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "828558977430847494", + "chan_point": "b5f98866d566ef73a1373172f4ba2f0ff37c4d9a75e425a131c6626a58b8bb07:6", + "last_update": 1710796902, + "capacity": "30000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710790857, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "26730000000", + "last_update": 1710796902, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "829339630716649475", + "chan_point": "659b3e91ea53c7f2401f14c7f1ca98587330ee94355f29ca185cf97d95c8149e:3", + "last_update": 1710838630, + "capacity": "30000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1499", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710803158, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710838630, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "830314897423532033", + "chan_point": "14401ca9d16f4eb0c144b1b682ee4c01a1b046b7e234ff4227985c0af7668170:1", + "last_update": 1710822101, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "4455000000", + "last_update": 1710822101, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "50000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "735", + "disabled": false, + "max_htlc_msat": "2722500000", + "last_update": 1710790858, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "830941619156811776", + "chan_point": "05b504514e3aaef84321d1b3a9bd20510b1fad91203080ecd7bc1a787a277b21:0", + "last_update": 1710718954, + "capacity": "10100000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "9999", + "disabled": true, + "max_htlc_msat": "9999000000", + "last_update": 1710655516, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "10100000000", + "last_update": 1710718954, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "831240686274674689", + "chan_point": "5053cc618432941e788d246cadc7f07d5c47e031eb0b86889ed1cf3d8fe0e5c9:1", + "last_update": 1710825484, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "60", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710394665, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "node2_pub": "030066a41ce86e73e96ffcbc3cf445744d5c92144e0c254c421a0204aebbdbb2bc", + "channel_id": "832548005667143681", + "chan_point": "7548e1fb9f5c2a91680fd6393688c52c95b339ec8edad48d32584cb35b444af5:1", + "last_update": 1710025671, + "capacity": "70000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "400", + "disabled": true, + "max_htlc_msat": "62370000", + "last_update": 1710025671, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "69300000", + "last_update": 1665612046, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "channel_id": "834551315785383940", + "chan_point": "bbd3917761c03326819660e20670b1b72c599d2f0c4ba0844946fce61d4a1b37:4", + "last_update": 1710794446, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710089878, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": true, + "max_htlc_msat": "30000000", + "last_update": 1710794446, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "834677759617662977", + "chan_point": "7af18f3ee5ebf69f26b04c30f9003d1a2125e6266ba650fb9806b5e0444c18b7:1", + "last_update": 1710822654, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710790997, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710822654, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03fe320dd1a656842bf4cf31e25fbe76f18ae1d3ac35c477e32063b360024fd956", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "835349561197789185", + "chan_point": "29ab4a9ab9f30b30f7ade78fdbdfbba8c29ac589c4fda9356c38186917a81854:1", + "last_update": 1710817104, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710579861, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "70", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710817104, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "835373750426664960", + "chan_point": "0abcdcda4fd407d72cac18d53985a8186d298b59953dfeba2bac40d41ed76b96:0", + "last_update": 1710681712, + "capacity": "28000684", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "28000684000", + "last_update": 1710604631, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1", + "last_update": 1710681712, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "835449616896294913", + "chan_point": "65e1dabac108d5939cfe10b9870de75cd5927316cd87ec865fd58eabfa091628:1", + "last_update": 1710845427, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710771857, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710845427, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "835488099714072577", + "chan_point": "358c3b77f7f02d5fbfbab039b2ff7acfbe359a8a71b7e235795becb5a2caa8d9:1", + "last_update": 1710849654, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "485", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710836574, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710849654, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "029a0d058126445a179586a104a8425bc996b88b0bfc533eed34f49d34b1c3b684", + "channel_id": "835800361030778880", + "chan_point": "1d539d646510790ce6a77d65e5784b44f6a325826b9b6ad05aba5bdf18272aab:0", + "last_update": 1710774014, + "capacity": "1100000", + "node1_policy": { + "time_lock_delta": 450, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "1100000000", + "last_update": 1710756547, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "800000000", + "last_update": 1710774014, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "029a0d058126445a179586a104a8425bc996b88b0bfc533eed34f49d34b1c3b684", + "channel_id": "835819052721373185", + "chan_point": "a0b9f7e77f1ff467e581b3552d1ebffe39e4363fb634721da19514eb3d7dc7aa:1", + "last_update": 1710837547, + "capacity": "1100000", + "node1_policy": { + "time_lock_delta": 450, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "1089000000", + "last_update": 1710837547, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "1089000000", + "last_update": 1710808254, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "channel_id": "836482058179837953", + "chan_point": "e2ca412e9c26593e47e2c1d231cfd63b0b761a421e5df566196ff72b06796003:1", + "last_update": 1710783054, + "capacity": "33554432", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "608", + "disabled": false, + "max_htlc_msat": "33218888000", + "last_update": 1710122277, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": true, + "max_htlc_msat": "33218888000", + "last_update": 1710783054, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03fe320dd1a656842bf4cf31e25fbe76f18ae1d3ac35c477e32063b360024fd956", + "node2_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "channel_id": "836551327455969281", + "chan_point": "95cb9b81b29212d1290f66810dfa192010de60c3eb4833a0d30af9851032b6d0:1", + "last_update": 1710829704, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "70", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710712237, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710829704, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03fe320dd1a656842bf4cf31e25fbe76f18ae1d3ac35c477e32063b360024fd956", + "channel_id": "836585412261314561", + "chan_point": "ea7201810a997a2bf1474cc0f21d3857b5dc36260f50f018b49aa1f6d69566b5:1", + "last_update": 1710716304, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710712267, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710716304, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "836827304877162497", + "chan_point": "60f21c1650649ba1d5976ecfc14b8e28a6b3924ec688650bb940677dbf164b22:1", + "last_update": 1710805974, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "9999", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710778343, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710805974, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "node2_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "channel_id": "838041165645676545", + "chan_point": "97789bc894484fa48367670eed6f02a619820ca69a60a81ef7672c7f572cc6ac:1", + "last_update": 1710806734, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "19", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710806734, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710794910, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa", + "channel_id": "839705826268413957", + "chan_point": "ed8cf55ac1db0368bed8f8085d5933fef592c5fc6f229226e997bee8295f0e60:5", + "last_update": 1710794839, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710780197, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "224", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710794839, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "node2_pub": "03fb1e396cc523f9823458197150b74fdc79b0478ee00f67f526cd1e14e1ad267d", + "channel_id": "839889444694392833", + "chan_point": "3a3d1d8314529a7729701539e3f68a5cc5cf92d5e091bcb3cbe93d9a6bd1befd:1", + "last_update": 1710856110, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710856110, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710816528, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "840281970384240641", + "chan_point": "1037bb8d891e57fcb2255dbc84ce262b64abe8625edcb98168735f645d8f7daf:1", + "last_update": 1710631531, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1690812878, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "475", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710631531, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0238268dc72b78f0bc19398b46916246723c013911286b91b61e7f78d7a9a90174", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "840879005253435392", + "chan_point": "2e0dc71846b9215b80ece290092e2572b8c00821e862a66da32fafdf8697d931:0", + "last_update": 1710807040, + "capacity": "1832587", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "543", + "disabled": false, + "max_htlc_msat": "1814262000", + "last_update": 1709953781, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "280000000", + "last_update": 1710807040, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "03fe320dd1a656842bf4cf31e25fbe76f18ae1d3ac35c477e32063b360024fd956", + "channel_id": "841391377607360512", + "chan_point": "cf7e20ce33e80514e8b166b719599098946b25207042d355e3b2cb63e6452050:0", + "last_update": 1710804504, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710799948, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710804504, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "842115955863977985", + "chan_point": "29216f31fe8e4fb3a88c7af2211f0d5d8a3cd8d9d611643cd195f083c6de9568:1", + "last_update": 1710844534, + "capacity": "3380000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "49", + "disabled": false, + "max_htlc_msat": "3380000000", + "last_update": 1710844534, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "3346200000", + "last_update": 1710619552, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0246ee8e4c965296799eebd29a0948b9a4641843298b0f2a8e42256c4b594e4b8f", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "843135203032432640", + "chan_point": "56e1113c87bb0cf7e3e633d781db6b39cdc992b59c9d9e8601c14d18211b1acf:0", + "last_update": 1710815230, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "750", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710800772, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710815230, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "channel_id": "843567311088451585", + "chan_point": "dcb65184c2e1dfc8c8f3fece5551d57555034a83f57242ad5e045830094ab13b:1", + "last_update": 1710803931, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710803931, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1701256078, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0246ee8e4c965296799eebd29a0948b9a4641843298b0f2a8e42256c4b594e4b8f", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "844152251268005889", + "chan_point": "009eade7bacb5b98238154af06bb556f0fb14a9a94bb25b169b98c0d15579f03:1", + "last_update": 1710757572, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710757572, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710348140, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "844818555359657984", + "chan_point": "1636b5bcd90255cda029c661ade3f4ebcdb42453db962b4d9a1126dafdd69d53:0", + "last_update": 1710831407, + "capacity": "7947136", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "4730", + "fee_rate_milli_msat": "1272", + "disabled": false, + "max_htlc_msat": "7867665000", + "last_update": 1710831407, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1345", + "disabled": false, + "max_htlc_msat": "7947136000", + "last_update": 1710689767, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "845631094439870468", + "chan_point": "ade46379cee4582ac1bccd1c1825e1dc50632ec36afc12e0d1815df1df44288a:4", + "last_update": 1710799828, + "capacity": "11000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2199", + "disabled": false, + "max_htlc_msat": "10890000000", + "last_update": 1710799828, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "10890000000", + "last_update": 1710799828, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "845717955864035337", + "chan_point": "f8a5eb2ad759c134c7dd93a43b71694e46f1a3a22a4868cd43500f5de6bb2d6c:9", + "last_update": 1710856301, + "capacity": "21000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710790865, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "998", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "18711000000", + "last_update": 1710856301, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "029ed7e456a53568e12f013173dc5be2a92c86302e15038d150362933b754e5d21", + "channel_id": "846207238618349569", + "chan_point": "beaf49ad8f5b79c08eea9185eed29f9877466d718bb9052a0f06a942f755bd64:1", + "last_update": 1710828054, + "capacity": "5100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "55", + "disabled": false, + "max_htlc_msat": "5049000000", + "last_update": 1710827141, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "5049000000", + "last_update": 1710828054, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "847062658600927235", + "chan_point": "ef963a1a111df59aae61f7ce065c89e93add544c8a85787bd8d5be85160af9e8:3", + "last_update": 1710803440, + "capacity": "7000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "6930000000", + "last_update": 1710779221, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "10000000", + "last_update": 1710803440, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "847062658600927238", + "chan_point": "ef963a1a111df59aae61f7ce065c89e93add544c8a85787bd8d5be85160af9e8:6", + "last_update": 1710853841, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710853841, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710795330, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0246ee8e4c965296799eebd29a0948b9a4641843298b0f2a8e42256c4b594e4b8f", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "847220988269428739", + "chan_point": "dbb0527b9a00d1b86d301b47af5a75175f53528444ac9d890e30f8551b9bec78:3", + "last_update": 1710822101, + "capacity": "6000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "80", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710790856, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "5346000000", + "last_update": 1710822101, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "847360626226429953", + "chan_point": "661a41751b5adb52329833d9171487e490b7f9c7a5bdde171ee312c6e6836969:1", + "last_update": 1710757232, + "capacity": "10100000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1953", + "disabled": false, + "max_htlc_msat": "10100000000", + "last_update": 1710757232, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "9999000000", + "last_update": 1710085980, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "029ed7e456a53568e12f013173dc5be2a92c86302e15038d150362933b754e5d21", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "847929073754636288", + "chan_point": "804e511c9972de3033dca820a4edd1191771ea7fe13c729743954eb2e31168e3:0", + "last_update": 1710855941, + "capacity": "10865362", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "215", + "disabled": false, + "max_htlc_msat": "10756709000", + "last_update": 1710855941, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "10756709000", + "last_update": 1710754081, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0246ee8e4c965296799eebd29a0948b9a4641843298b0f2a8e42256c4b594e4b8f", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "848085204485799937", + "chan_point": "05367bf193cd8765b4d4435b8883c2c64cbe034a3d2a1f36c45cbca4633f89c0:1", + "last_update": 1710838572, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710838572, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710793854, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "848783394325004291", + "chan_point": "3f6e9f9880010cc94a25f5f6ac0a482444be1b3d130fb07977b64d6f5c391548:3", + "last_update": 1710770243, + "capacity": "11000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "10890000000", + "last_update": 1710770243, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "10890000000", + "last_update": 1710696129, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "848882350473347075", + "chan_point": "7005d695f343bf9bd9cb2e7570c915847d576cea41051c451ef5009a3a741467:3", + "last_update": 1710856469, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "8009", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710602826, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710856469, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b9735190f3e98ee3d96f0cef0bcd02af2ee37141d5c2d969683b354e2293fd8f", + "node2_pub": "022ea68ac7588959a6a4829a5ff176c296329d614b90b3f5f4456b5d6130ea50f6", + "channel_id": "849038481014980609", + "chan_point": "12fcc7926fd12bca65bfbb7f4115b736c711fb5050ee5b2c3df60d3faab2716a:1", + "last_update": 1710817552, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "224", + "disabled": false, + "max_htlc_msat": "1956000000", + "last_update": 1710817552, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "99000", + "fee_base_msat": "750", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "22000000", + "last_update": 1710788729, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "849343045792432129", + "chan_point": "bd77b9808da30f57b75708f79a926a995fb8069fe34c8e96a725e609adb404a9:1", + "last_update": 1710827331, + "capacity": "30000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "813", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710827331, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "849371633002348545", + "chan_point": "537a13bb91fa7785f66615e372194a74f99c3a4672d317f98dff5bc152d910c1:1", + "last_update": 1710856490, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "487", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710856490, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710856489, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021aa6f0fdc8ea3c85b98aff27e5ffe1f67921e75e7aa7bad6a36d6085d54117df", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "850774609906106368", + "chan_point": "50a4305afc057f342e1b4686c225ccb2e46bdfc06996b40a3dd8b29c34f9fc07:0", + "last_update": 1710786320, + "capacity": "30934", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "30625000", + "last_update": 1710786320, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "30625000", + "last_update": 1710611821, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "036cb1a035eb1c7f125c004ed046d329285bd57e4100f18b577af721659becd832", + "channel_id": "851304574543790096", + "chan_point": "fde30f6c5e6545f3bf5b7bbe7fb47c3f261614b336be7ab8d037909a8c931463:16", + "last_update": 1710823901, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710790869, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "13365000000", + "last_update": 1710823901, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021aa6f0fdc8ea3c85b98aff27e5ffe1f67921e75e7aa7bad6a36d6085d54117df", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "851467302217777153", + "chan_point": "ac6eb42bf4d03fd2a2cccd22ee95c6927f9b6a95eab3760cac0746aec81fd4a2:1", + "last_update": 1710807858, + "capacity": "20000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000", + "last_update": 1710719720, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000", + "last_update": 1710807858, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "022ea68ac7588959a6a4829a5ff176c296329d614b90b3f5f4456b5d6130ea50f6", + "node2_pub": "0303e0423fd2c40d73978b03069f63d6ca5ae1abf61f5666bbbea886181f21e8e3", + "channel_id": "851853230885109761", + "chan_point": "97513e54974887b73ba45289d77d089bf6608191a19fd7f7367ccc94b8c9139e:1", + "last_update": 1710847222, + "capacity": "2500000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "166", + "disabled": false, + "max_htlc_msat": "2475000000", + "last_update": 1710817552, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "145", + "disabled": false, + "max_htlc_msat": "676724000", + "last_update": 1710847222, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "852275443262881792", + "chan_point": "05bc8e62087476abf0b6457cb451e109d6a919bb5204ff22584dfb52c12b6b2b:0", + "last_update": 1710807943, + "capacity": "6900000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "90", + "disabled": false, + "max_htlc_msat": "6831000000", + "last_update": 1710660474, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "6900000000", + "last_update": 1710807943, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "852421678319665157", + "chan_point": "0741193526c43dc659947228f037dcc40bb73a70423775e62095c674ecf8282a:5", + "last_update": 1710852439, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710782821, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "304", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710852439, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "852525032407236609", + "chan_point": "b455866536c2a706db48622e1d976888cd6ba40e0cd981bfded493fe3c9558b2:1", + "last_update": 1710843032, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "6", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710843032, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710826840, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "node2_pub": "027d4f8fd79e846b19c36891004c4e1c02e4ec6452112dd73a5761c971cf880ee5", + "channel_id": "852606396236103680", + "chan_point": "18f5e4ccbcb40634fc6bc65dea251a0af52c33bfad24050ba9b1329dba36a8bd:0", + "last_update": 1710785663, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710061704, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "100000000", + "last_update": 1710785663, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "channel_id": "852847189292351489", + "chan_point": "8305315fe045996c70e1b5203f9f23ac33adfd7a2ee202e86bd3a945504a90da:1", + "last_update": 1710814352, + "capacity": "501115", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "496104000", + "last_update": 1710814352, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "496104000", + "last_update": 1710814232, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "028a7930a96f7a604014a8d6874004262ea64aea4def90839c265f8c7ff8a4b34c", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "853211127701635073", + "chan_point": "1510463d74758c5fa1d70b7c6b690149884800ed98bf7245a364aa4ff573dd7b:1", + "last_update": 1710849282, + "capacity": "300000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "297000000", + "last_update": 1710849282, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "550", + "disabled": false, + "max_htlc_msat": "297000000", + "last_update": 1710789264, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "853612449362935808", + "chan_point": "96d83d0eb907367e794440b91ed86c13dc0eb143ab7df3ddbb4dbc21592555b7:0", + "last_update": 1710845743, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710845743, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710593844, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "027d4f8fd79e846b19c36891004c4e1c02e4ec6452112dd73a5761c971cf880ee5", + "channel_id": "853824655087828993", + "chan_point": "93b1d1efb4003f88e9bddcd1b0560e6129ce7dbe8f49fe63fbac47eccf727f6e:1", + "last_update": 1710785354, + "capacity": "1500000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "1485000000", + "last_update": 1710061705, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "1500000000", + "last_update": 1710785354, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "854116025735643136", + "chan_point": "b4dd72c7e215c6e35ba98ba30347b6d0386fc816b5e3695a1cec401d8abd3031:0", + "last_update": 1710843032, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710816935, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "11", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710843032, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "854252365134299137", + "chan_point": "b74cd9820a4be1d74ae14bde9037a919ab4cbfbc4c00bcb121c3d6b8d6bb96d2:1", + "last_update": 1710829207, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "239", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710829207, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b9735190f3e98ee3d96f0cef0bcd02af2ee37141d5c2d969683b354e2293fd8f", + "node2_pub": "03ca2ce661ceb09c245588b8f4cb5dd041e387dca7d33ff3754eeb3928553aac98", + "channel_id": "854700965874368513", + "chan_point": "2d9f3ea67203bde8db39e50c7a14e1c4c129525d5113dc63a9541fd0caeb49cd:1", + "last_update": 1710844294, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "968000000", + "last_update": 1710649488, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710844294, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "854929664299827200", + "chan_point": "1434652e9f165c89fe4bcf8b702f8c5b0aaaa7a8a9fb696206f81e7a01a9345f:0", + "last_update": 1710829154, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "800000000", + "last_update": 1710829154, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "298", + "disabled": false, + "max_htlc_msat": "1000000000", + "last_update": 1710757039, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "855098989111869441", + "chan_point": "777f4738859fad58185ba5144c02da3d35dc2c5ef2c8159a7a900a70656ca226:1", + "last_update": 1710843032, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1460", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710843032, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710802681, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "channel_id": "855298000662102017", + "chan_point": "6614b1f0e5deff9cb22dafe11960db09fde32cc36eb0aca017e601cd02c050d0:1", + "last_update": 1710814807, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710814352, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710814807, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "855687227868250113", + "chan_point": "c1331b8a88f458d538115fbbdb67eb52c7521296e72874a34e6e5c4250110eb4:1", + "last_update": 1710843033, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "787", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710843033, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710817254, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "022c6a25c6090a64b2710ef7f15740c4a000c480ef16a3b5845864af448cb322ba", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "857312306009145344", + "chan_point": "fccdafb6af4011be5ab317254b2eeaa32b6607ba97c97badbe066f00493efbb4:0", + "last_update": 1710789264, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "150", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710787765, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "700", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710789264, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "channel_id": "857942326103179265", + "chan_point": "ae92472c182f1aba9da85c73dc1fbbaffa7e8fd502729b0d83002923e625c051:1", + "last_update": 1710856800, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710821548, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "63", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710856800, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "858245791320571905", + "chan_point": "7815ce2c19b877ab55619d042ac17ba2a8d16888f8eb66faa8115988f9f79a19:1", + "last_update": 1710829457, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710829457, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "858552555091591171", + "chan_point": "8f56547f78665a50c8f2b5ba7453a2c4abcee9528ae7d2887a2418e8421e343c:3", + "last_update": 1710849922, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "410", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710810362, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "104", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710849922, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "858552555091591175", + "chan_point": "8f56547f78665a50c8f2b5ba7453a2c4abcee9528ae7d2887a2418e8421e343c:7", + "last_update": 1710812663, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710786922, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710812663, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "858566848720732163", + "chan_point": "b881d3a276b344af7e3e70c44b98729b3c0104ce5290960392d544526dfda915:3", + "last_update": 1710840654, + "capacity": "30000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710840654, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "61", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710772522, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "858566848720732164", + "chan_point": "b881d3a276b344af7e3e70c44b98729b3c0104ce5290960392d544526dfda915:4", + "last_update": 1710583522, + "capacity": "30000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710203740, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710583522, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "858566848720732165", + "chan_point": "b881d3a276b344af7e3e70c44b98729b3c0104ce5290960392d544526dfda915:5", + "last_update": 1710637522, + "capacity": "30000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1709340874, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "38", + "disabled": false, + "max_htlc_msat": "29700000000", + "last_update": 1710637522, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "860461307324923905", + "chan_point": "171a9db90339f0417f14ab00a58f3b28d02a38b79077ae3d2cc49f6077322eaa:1", + "last_update": 1710807858, + "capacity": "8105381", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "8024328000", + "last_update": 1710807858, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "8024328000", + "last_update": 1710805491, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "860958286649425922", + "chan_point": "8f12bfefb5777e2d4e3c9028f65ad84979e0ab4083e7e390c87d422ce8f88ad0:2", + "last_update": 1710829039, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710782735, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "214", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710829039, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "860958286649425926", + "chan_point": "8f12bfefb5777e2d4e3c9028f65ad84979e0ab4083e7e390c87d422ce8f88ad0:6", + "last_update": 1710806233, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710806233, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "297", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710793039, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "861423380097138692", + "chan_point": "67eb0e8d784817da6cf6c9253924b6df24839d4565f4ec17c57464134cc9284c:4", + "last_update": 1710850636, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "3953", + "fee_rate_milli_msat": "277", + "disabled": false, + "max_htlc_msat": "50000000000", + "last_update": 1710768442, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "13003", + "fee_rate_milli_msat": "65", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710850636, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "861423380097138693", + "chan_point": "67eb0e8d784817da6cf6c9253924b6df24839d4565f4ec17c57464134cc9284c:5", + "last_update": 1710856501, + "capacity": "130000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "5300", + "disabled": false, + "max_htlc_msat": "128700000000", + "last_update": 1710856501, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "128700000000", + "last_update": 1710856500, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "channel_id": "861519037497737220", + "chan_point": "4cb6d3d3eb1cc1bc6c9371ef77dfd85556690f37c8cb80b240403cb84a274b82:4", + "last_update": 1710843607, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710798929, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1235", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710843607, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "861735641247449091", + "chan_point": "049562f40a1e281238f5f4f6c23858cd27ec6da08d7d946fd36d2383c89eb556:3", + "last_update": 1710846600, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "315", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710846600, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710838363, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03d4809d1a129228b9afb54dd5b3bcc29b8e59d5c8fc1f3104315baf19c8e0c76b", + "channel_id": "862864839716372481", + "chan_point": "2d7228d7df2158148d1da4e3be99db7479eef9a4c44b004297e7835ef99a5d68:1", + "last_update": 1710851454, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710851454, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "450", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710839263, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03d4809d1a129228b9afb54dd5b3bcc29b8e59d5c8fc1f3104315baf19c8e0c76b", + "channel_id": "863081443571073025", + "chan_point": "afadbf08cd122a91cbfd79b481e067fbaee33cbf2f705c2b95b8446e69a80d99:1", + "last_update": 1710831429, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710831429, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1100", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710781663, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "864244726917431297", + "chan_point": "42147b93add7c26897e5062d278dc4553e67fb3cfc85fc20a214df4f816192d9:1", + "last_update": 1710800407, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710799031, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1375", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710800407, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "channel_id": "864361275012743169", + "chan_point": "c44360fc1fb935c38c733a34051b8139211caa02bb4aab37a4aaacaccd95443c:1", + "last_update": 1710843033, + "capacity": "3200000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "3168000000", + "last_update": 1710803071, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "162", + "disabled": false, + "max_htlc_msat": "3168000000", + "last_update": 1710843033, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "864505311075368960", + "chan_point": "caff4478d3275c81ab34f076b8bad1b5f4d29e5fc600877c55ce44f920e0b0b7:0", + "last_update": 1709875367, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "599", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1709636731, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1709875367, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "node2_pub": "027d4f8fd79e846b19c36891004c4e1c02e4ec6452112dd73a5761c971cf880ee5", + "channel_id": "866072115312787457", + "chan_point": "4c3bc762430ff6b7c2571a1c74b0602eb72382dd90a5337220c52881f2090381:1", + "last_update": 1710847457, + "capacity": "1500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1500000000", + "last_update": 1710847457, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "1485000000", + "last_update": 1710631189, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "867668606052073473", + "chan_point": "592dd4151c78bc6f240c59ec4aa0375a25b38801163e8ff18e286e029606dd94:1", + "last_update": 1710786335, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710786335, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1706575582, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "channel_id": "868494339200909313", + "chan_point": "e03908a9bfaa2366b68a9c105b189a434379261d583ae8fce1d848a403525f3c:1", + "last_update": 1710851954, + "capacity": "2500000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "11", + "disabled": false, + "max_htlc_msat": "2475000000", + "last_update": 1710845427, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "2475000000", + "last_update": 1710851954, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "869129857120993282", + "chan_point": "78c5d2798c3eedc24aba7b71b9ab4618e71d97d3857fc79c5aa12009c5ed5f2b:2", + "last_update": 1710820457, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710820457, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710805807, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "869744484135600129", + "chan_point": "9563b4ff69a191f6b7b613b9978e8a4d05cf92037ad4ffd6e08139e69b9c9895:1", + "last_update": 1710855641, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710778731, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710855641, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "869744484135600134", + "chan_point": "9563b4ff69a191f6b7b613b9978e8a4d05cf92037ad4ffd6e08139e69b9c9895:6", + "last_update": 1710853270, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710814353, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "86", + "disabled": false, + "max_htlc_msat": "14840000000", + "last_update": 1710853270, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03a21a8756ffc55dd1d83a02b0cc2e163f8d041e3f56b309232e5389c66c00364c", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "870145805817085953", + "chan_point": "7e3fe6961fb04b0d6bb332f8a5029aac660f912931ae93116361b54d8825c5c9:1", + "last_update": 1710848160, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710783891, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710848160, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "870162298416070667", + "chan_point": "2b6b9fd1a87fd9adaccc7c00171a74bcbf7a3a75eae3c02f6a6b9e8c2cffb3fd:11", + "last_update": 1710854804, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710854804, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710796154, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "871372860814393345", + "chan_point": "5fbf1bed89c9673723390d85e87fc351e3ed4cae91d128dee852756baff516db:1", + "last_update": 1710810979, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "4725", + "fee_rate_milli_msat": "2362", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710810979, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "0", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710237045, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "871581767934869505", + "chan_point": "3a8aaacae4315d338db60807db6cfe8d8564ab3963fdaba4be59b75a975b64ce:1", + "last_update": 1710835032, + "capacity": "40000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "4083", + "disabled": false, + "max_htlc_msat": "39600000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "39600000000", + "last_update": 1710835032, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "871868740602232834", + "chan_point": "e3d4dbe173d3b2b187de3668097d6c659f84f7bb4568f0899f951882c1bde729:2", + "last_update": 1710829232, + "capacity": "10100000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "945", + "disabled": false, + "max_htlc_msat": "9999000000", + "last_update": 1710829232, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9999000000", + "last_update": 1710803931, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "872036965794447362", + "chan_point": "fc6de16a69a24154566282822ebc705dfc75724bdd841d835af377d41e8ed06e:2", + "last_update": 1710788484, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710777850, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710788484, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "872385510883131399", + "chan_point": "f60a3a740eb3c8229164018d706941801c0c7a0677d279101daa8de699a48479:7", + "last_update": 1710814008, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1", + "fee_base_msat": "1", + "fee_rate_milli_msat": "327", + "disabled": false, + "max_htlc_msat": "600000000", + "last_update": 1710814008, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "40", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710804922, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "872385510883131400", + "chan_point": "f60a3a740eb3c8229164018d706941801c0c7a0677d279101daa8de699a48479:8", + "last_update": 1710855054, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "1", + "fee_rate_milli_msat": "180", + "disabled": false, + "max_htlc_msat": "1000000000", + "last_update": 1710814008, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710855054, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "872443784965980161", + "chan_point": "9838f109ffd5875afa7ab83e656726c83b586a0b19060de5b42d43bd7f95ee09:1", + "last_update": 1710838621, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710838621, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710788281, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03c41fde73d7853d07c0b0baf110e91c5f54f768cc94d58ddc0d3e947fa7d27e29", + "channel_id": "873894040860622849", + "chan_point": "86225d5500aedd394e172df3aa4dc4e6bd13f5e7dc693543e75df9c5400a9ff2:1", + "last_update": 1710754254, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710754254, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1709510190, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "channel_id": "873973205729280001", + "chan_point": "f01825aeb06b0a401599be0b8c401c35d28e2ef08ff34593f83f618eed8e4c39:1", + "last_update": 1710848353, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710785048, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710848353, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "874017186217918464", + "chan_point": "a5fc35368281ffbdc696b06bdcb23f3e563977ef9f99b55faf901efc9d917fae:0", + "last_update": 1710772753, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710681712, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "5000000000", + "last_update": 1710772753, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa", + "node2_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "channel_id": "874659301032722433", + "chan_point": "3f053f0b81f641f583568d3893cdd12f8ac7dbe3449ea733876157a4a1b7a23b:1", + "last_update": 1710817997, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "4000000000", + "last_update": 1710817997, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1709474894, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "874673594643185670", + "chan_point": "395bfd28de332b83abb59ba0e470672382f1497e163fd596c538128307011543:6", + "last_update": 1710814989, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710794564, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710814989, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "node2_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "channel_id": "874694485266268160", + "chan_point": "b9c28e5e616473ce5a0258d3fc28fac5a34c1047beb62f13a028e4557196b1a9:0", + "last_update": 1710826513, + "capacity": "1993097", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1973167000", + "last_update": 1710794020, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": false, + "max_htlc_msat": "1973167000", + "last_update": 1710826513, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "874841819846541313", + "chan_point": "5c7a3e4f467f08f1c29954eff6ff08b1efdd5ccdd803918240e5a058d146d619:1", + "last_update": 1710853280, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710788758, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "1760000000", + "last_update": 1710853280, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "channel_id": "874848417047838721", + "chan_point": "0b337d91493ec80db5bc84134553a22f2bb22a2426e7d6242813f974ac8e6048:1", + "last_update": 1710819748, + "capacity": "14000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "14000000000", + "last_update": 1710819748, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "13860000000", + "last_update": 1709824082, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "channel_id": "874942974993629186", + "chan_point": "ee8d03bb7d3aea4037e9727484def09cbe1f2730f7fb3b61f3ec14705c0ee6b3:2", + "last_update": 1710818239, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "306", + "disabled": false, + "max_htlc_msat": "1000000000", + "last_update": 1710818239, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710445964, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "channel_id": "875337699844816898", + "chan_point": "3c0ee164148756ea89f9ff517add780a0a5a7461d93a575b675635f9935bfed8:2", + "last_update": 1710779620, + "capacity": "2500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "2475000000", + "last_update": 1710779620, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "80000000", + "last_update": 1710769241, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "875337699844816899", + "chan_point": "3c0ee164148756ea89f9ff517add780a0a5a7461d93a575b675635f9935bfed8:3", + "last_update": 1710812441, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710812441, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710777922, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "875337699844816900", + "chan_point": "3c0ee164148756ea89f9ff517add780a0a5a7461d93a575b675635f9935bfed8:4", + "last_update": 1710803441, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "140000000", + "last_update": 1710803441, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1985", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710779131, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "875850072099651585", + "chan_point": "a758034965f930a90a8a8e7e5b2553dd427294a5c2140aba3ae502372ffbcc18:1", + "last_update": 1710853110, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710842650, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "100000000", + "last_update": 1710853110, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "876652715588321282", + "chan_point": "7671908e6dbb224458a79282cd53d1c165cb7c02f16c5e76126d7bede9a5925b:2", + "last_update": 1710805807, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "623", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710798632, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710805807, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "03fb1e396cc523f9823458197150b74fdc79b0478ee00f67f526cd1e14e1ad267d", + "channel_id": "876747273627631616", + "chan_point": "7ebb9b7609486d9d86fdcc111a27a83fefe70b60820b93dfda51ebb843096576:0", + "last_update": 1710814728, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710814355, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710814728, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e114a6bffa9ff420eb84491f3eceb3b2a5a5638f6d60d4324e16436c61acdcdc", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "877328915132317697", + "chan_point": "d8af5721b580140894c8f8ea7195b7e83edc07a86e6e24c8efa620e5fabedbfe:1", + "last_update": 1710836501, + "capacity": "2500000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "1125000000", + "last_update": 1710604842, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1500", + "disabled": false, + "max_htlc_msat": "1125000000", + "last_update": 1710836501, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "879261856582598657", + "chan_point": "28865bb2a78214d1fb70d840ff310c552fc7fb325e31fca08da8da6c3e859abc:1", + "last_update": 1710843033, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710818821, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "408", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710843033, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021d2436cab847373a4212bf6d754ead5304f5d0791479643893a837b295f3441c", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "879261856626966529", + "chan_point": "8d972be2ed57e9bf954efc0b0505baeed5d1fcce7e8c4fea5a6ea58e5d8619f3:1", + "last_update": 1710786991, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "77", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710786991, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710771093, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021d2436cab847373a4212bf6d754ead5304f5d0791479643893a837b295f3441c", + "node2_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "channel_id": "879293742446936064", + "chan_point": "6a02bcab54f063af738ab7ace6e5268e820fadc70883959d3ef6cac89a064ad4:0", + "last_update": 1710843033, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "4", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710786990, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "112", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710843033, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "0303e0423fd2c40d73978b03069f63d6ca5ae1abf61f5666bbbea886181f21e8e3", + "channel_id": "879916066066464769", + "chan_point": "68876042d3c7fc84b3694756da25c0e1e65fa4098d9e16894f837460396815a9:1", + "last_update": 1710847222, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "915", + "disabled": false, + "max_htlc_msat": "1115397000", + "last_update": 1710847222, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "4500000000", + "last_update": 1710210235, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "880673629523804161", + "chan_point": "457b7a0adac9ee0c43407552bd06cc0dbee5abe62da4da9916b5a4df9bb5b7f5:1", + "last_update": 1710841153, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710779929, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710841153, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "880829760205291521", + "chan_point": "1b832daa4a4a72efd68ca390dc01f03e4a6597d6de6a8850af39d6794dd1b1ef:1", + "last_update": 1710853278, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "189", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710789632, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "287", + "disabled": false, + "max_htlc_msat": "4930000000", + "last_update": 1710853278, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "03fb1e396cc523f9823458197150b74fdc79b0478ee00f67f526cd1e14e1ad267d", + "channel_id": "880960602030997505", + "chan_point": "ad6e1f232934cac85ccec5c1bb37fb505a29a681ff23215654a408ef5256c3b9:1", + "last_update": 1710816528, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710796154, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710816528, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "881278361018957825", + "chan_point": "6a4b3b530fe4f4aadd1272a3351f5ad716bea8c71e53f08cb34f474b882f8082:1", + "last_update": 1710805240, + "capacity": "3991937", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "3952018000", + "last_update": 1710779281, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710805240, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "channel_id": "882651651019177985", + "chan_point": "1d0cc9ce5b74bb2a6d011c8b1a85eca97bf4228dce944fa64ebfd814302088be:1", + "last_update": 1710845731, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710784632, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2485", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710845731, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "883062868309508098", + "chan_point": "d461a44bf2c984981a10c864200da7c051ce76c2eef9023c944761398f5c3378:2", + "last_update": 1710807858, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710807858, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "12", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710784039, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "883062868309508102", + "chan_point": "d461a44bf2c984981a10c864200da7c051ce76c2eef9023c944761398f5c3378:6", + "last_update": 1710805240, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "100000000", + "last_update": 1710805240, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "43", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710778639, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "channel_id": "883062868309508104", + "chan_point": "d461a44bf2c984981a10c864200da7c051ce76c2eef9023c944761398f5c3378:8", + "last_update": 1710784039, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710782331, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "301", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710784039, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "883062868309508105", + "chan_point": "d461a44bf2c984981a10c864200da7c051ce76c2eef9023c944761398f5c3378:9", + "last_update": 1710838854, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710838854, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "282", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710816439, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "883170620405907457", + "chan_point": "6e6160dd05bdca23efe934bfb53f0059d1736b18d77823ba00d8be0203414582:1", + "last_update": 1710825484, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "260000000", + "last_update": 1710777664, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "883852317749870592", + "chan_point": "63891404aff07ba780b969c1a77d34a7af1c3036d0c821bb2248cb97c8c9962c:0", + "last_update": 1710839213, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710779464, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "555", + "disabled": false, + "max_htlc_msat": "3000000000", + "last_update": 1710839213, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "884138190664040449", + "chan_point": "767617e8b706bcbac41f00621cabf986e01a360b5e40db331fd2a8aa128fa0d7:1", + "last_update": 1710855001, + "capacity": "7000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1450", + "disabled": true, + "max_htlc_msat": "69300000", + "last_update": 1710855001, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "6930000000", + "last_update": 1710841063, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "885149741433028609", + "chan_point": "4107a7f73589ca82ec54dfec294fe3341bfc0d7f2d1868cfd829939a20a9f504:1", + "last_update": 1710791398, + "capacity": "5500000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "680", + "disabled": false, + "max_htlc_msat": "54450000", + "last_update": 1710778441, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "5445000000", + "last_update": 1710791398, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "885190423360765953", + "chan_point": "935129cfbb3be5d579ef07f95bb3af4be0b3bba960a60052154246be91f0ced4:1", + "last_update": 1710845481, + "capacity": "16500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "399", + "disabled": false, + "max_htlc_msat": "16335000000", + "last_update": 1710827432, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "16335000000", + "last_update": 1710845481, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "channel_id": "885493888613941248", + "chan_point": "ca918cef40ba2df8f455f12c07f43a4c18ff7480c027d259f37de004c16a6e6e:0", + "last_update": 1710822654, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "50000000000", + "last_update": 1710822654, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1250", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710404038, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "channel_id": "885513679789293569", + "chan_point": "a71f6baad8bcc0eb7726d596f6c61a57d58527d8ab995c212fd585617af878e4:1", + "last_update": 1710817958, + "capacity": "1995967", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1976008000", + "last_update": 1710817958, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1976008000", + "last_update": 1710812020, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "886056838652755969", + "chan_point": "d4ceaa8ac97c64506fe6ca53d5b1a05ae28dae719f87f021ae2e0b9dad0e15c2:1", + "last_update": 1710854657, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9000000000", + "last_update": 1710854657, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "9000000000", + "last_update": 1710631492, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "channel_id": "886194277456412672", + "chan_point": "f3dd1145a8406d37d6604fd17574c8e1f6336c2a77fb8c790c0379904013995f:0", + "last_update": 1710803683, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "350", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710778781, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710803683, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "886308626709544962", + "chan_point": "4837f7a9dfc2d0cad1dc1d6d9f5dff2342eb12aa750f3db6d2654fcd82b6c81d:2", + "last_update": 1710803839, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "385", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710777032, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "99", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710803839, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "channel_id": "886493344707575808", + "chan_point": "c49fb182cfb29a69118369566af52fe86911f0242c2092ffde9fd890d12a4f11:0", + "last_update": 1710839832, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710839832, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1985", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710803683, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "022ea68ac7588959a6a4829a5ff176c296329d614b90b3f5f4456b5d6130ea50f6", + "node2_pub": "029a0d058126445a179586a104a8425bc996b88b0bfc533eed34f49d34b1c3b684", + "channel_id": "886573608920219652", + "chan_point": "898f2d355733eae6d3faaf63d956edd4b4c1ec6761c0833cbd9a1b60300dc841:4", + "last_update": 1710817552, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "150", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710817552, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 450, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710803347, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "channel_id": "886592300790185997", + "chan_point": "b118e2f5494e53302d817ef1132a207b01b08d0ee057cdf949c92f2ed87decf3:13", + "last_update": 1710791589, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790450, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710791589, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "886592300790186000", + "chan_point": "b118e2f5494e53302d817ef1132a207b01b08d0ee057cdf949c92f2ed87decf3:16", + "last_update": 1710791589, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "2000000000", + "last_update": 1710791589, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710681712, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "886619788490833928", + "chan_point": "fe88ddff03886631613cfb2bf0f80849f8074bb2f4269c05de8abcc110741e65:8", + "last_update": 1710791589, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710789935, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710791589, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "886619788490833939", + "chan_point": "fe88ddff03886631613cfb2bf0f80849f8074bb2f4269c05de8abcc110741e65:19", + "last_update": 1710843033, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "478", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710843033, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710791589, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "channel_id": "886632982537699334", + "chan_point": "75f178a4f984736b1210e62a4bbc66c9e7aee62b340d3aef67d9da7fd4549a7e:6", + "last_update": 1710822190, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710528519, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "2000000000", + "last_update": 1710822190, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "886632982537699336", + "chan_point": "75f178a4f984736b1210e62a4bbc66c9e7aee62b340d3aef67d9da7fd4549a7e:8", + "last_update": 1710856473, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1777", + "fee_rate_milli_msat": "199", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710791766, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710856473, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "0333175e2ddb8ae3fab14125c312cf62b9da6dc54fc922edd1aa11e4e059496594", + "channel_id": "886641778647564318", + "chan_point": "e79f7e0d0e5da93c547b3f9d75cd0a0647a4857ef6eafb5c26bf7d07200295f7:30", + "last_update": 1710791590, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "480", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710109678, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": true, + "max_htlc_msat": "1980000000", + "last_update": 1710791590, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "886759426467037211", + "chan_point": "5bc09590890f03e5e0a338c2352a2e3f87f30790814d056779b4881bea2cf68e:27", + "last_update": 1710846600, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710793390, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "316", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710846600, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "886767123023659009", + "chan_point": "10fe3292a03ca195c69ad861bb07b6e4c6486d9f6858c29533ce0d94d417aa97:1", + "last_update": 1710841048, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710810497, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "598", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710841048, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "032261d6ec5fb578919e2455f2061d27bd93df57b26d78cdf7bad38f1368d0531b", + "channel_id": "886775919173238800", + "chan_point": "d1cdbc9ff87bc82d71b42f1c2be1996581ddc914928990bdb7fe5d192e21558a:16", + "last_update": 1710779210, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "138", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710773240, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710779210, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "886802307381854213", + "chan_point": "64162d468ce15d175fbdce4c0d94b9047abf4400582da10236bcdca6623a7593:5", + "last_update": 1710807858, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710807858, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710805989, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03d4809d1a129228b9afb54dd5b3bcc29b8e59d5c8fc1f3104315baf19c8e0c76b", + "channel_id": "886811103491129349", + "chan_point": "e6e65cc949066dc849d8130f81bf8bfc9a52109c432318322053660ebe9deb1d:5", + "last_update": 1710790663, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710784389, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "180", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790663, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "886941945458982913", + "chan_point": "150cff20eb45b78b7c969134151ca85592dccab727d6e091a83fd0ebe3d1b8fc:1", + "last_update": 1710848832, + "capacity": "7000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "750", + "disabled": false, + "max_htlc_msat": "3150000000", + "last_update": 1710848832, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "3150000000", + "last_update": 1710605616, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "029a0d058126445a179586a104a8425bc996b88b0bfc533eed34f49d34b1c3b684", + "channel_id": "886973831192641543", + "chan_point": "541df3deb94bafbd71885609b51bc1de27187cc93d1705c3788a47508af7c2bf:7", + "last_update": 1710827589, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 450, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790747, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710827589, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "channel_id": "886973831192641573", + "chan_point": "541df3deb94bafbd71885609b51bc1de27187cc93d1705c3788a47508af7c2bf:37", + "last_update": 1710795189, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "49", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710792334, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710795189, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "887290490550484993", + "chan_point": "8aca0ea50a44772ebb0ffaec9a04a6e0d047a3e0c30403530686497ceb905504:1", + "last_update": 1710838517, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710838517, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710826522, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "887308082692030465", + "chan_point": "b634fad4611de0a11988fab615f6f175a6d781d497f7ee50266e3a951c44c4b7:1", + "last_update": 1710854909, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "25000000000", + "last_update": 1710814357, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "40", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710854909, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "887361958759432192", + "chan_point": "358dd54644e0ce38bd9c8d658b54bc5da28e3a9f4e1ca3979a87c31f951cb14a:0", + "last_update": 1710854911, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "54528", + "fee_rate_milli_msat": "41987", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710854911, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "100000000000", + "last_update": 1710775681, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "887465312824524801", + "chan_point": "a59dec9e18805e41b1f42ddc92210e6bc5f2a01cc3456548db641b298186ea90:1", + "last_update": 1710837519, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "2745", + "fee_rate_milli_msat": "914", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710837519, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "799", + "disabled": false, + "max_htlc_msat": "50000000000", + "last_update": 1710777958, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "887465312824721408", + "chan_point": "fbd30e889cd756b8e18b04c3175fb5e13bfa2c704527014cf55e030d841227e8:0", + "last_update": 1710837519, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "942", + "fee_rate_milli_msat": "471", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710837519, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "25000000000", + "last_update": 1710775131, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "887535681579319297", + "chan_point": "8622d9b19c80349dac710728ec72670e898ca6e59113db56d4c468970f34d90b:1", + "last_update": 1710843033, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710843033, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710838363, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "029f670766420210850a6c388b109d3a356896ff3adba3a0e262d4d8a74f225ff1", + "channel_id": "887725897158885402", + "chan_point": "ebdba1f9d2a671c43a5a7a2d15b0871bcb9d8c8dbd90f739f990c604220b5e5b:26", + "last_update": 1710845428, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "165", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710845428, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710793390, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "024efd67cb4e01cdcd79e86f561cb5e66faea6d3761b5f33daca6afce02a80a7df", + "channel_id": "887757782897721367", + "chan_point": "8ae029111d3e44a0ef02a19caa5ba36b41fa78f30e1eb76293822180760611f6:23", + "last_update": 1710796663, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710443151, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710796663, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "887850141923278849", + "chan_point": "5b774ae290201675d94d3d096a1fe7a8cae8b281445afd122bd3973ce18600ee:1", + "last_update": 1710787013, + "capacity": "7000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "6930000000", + "last_update": 1710786430, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "700", + "disabled": false, + "max_htlc_msat": "6666666000", + "last_update": 1710787013, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "888351519282954242", + "chan_point": "a3536a05411a39acd5f0afd6bcf48b03ac9a5b57e530698614a8c022c349f292:2", + "last_update": 1710853288, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4420000000", + "last_update": 1710853288, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1709913614, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "0215a106e71380f6b273e8232915f5fb3942822b1f8728d8d3ab8c9dc6eb2e8b02", + "channel_id": "888405395390595073", + "chan_point": "041bad6c7c41bf539933f9991ab45cbb0efff701ec74238a1d6a14158ebe2d82:1", + "last_update": 1710845796, + "capacity": "7500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "3375000000", + "last_update": 1710845796, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "3375000000", + "last_update": 1710488135, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "channel_id": "888414191473721344", + "chan_point": "1f79a56d029cefd483fa1dbd4110a453802d61b040fc4d36b543dac2a96c0243:0", + "last_update": 1710803684, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710803266, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710803684, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "0215a106e71380f6b273e8232915f5fb3942822b1f8728d8d3ab8c9dc6eb2e8b02", + "channel_id": "888497754257227777", + "chan_point": "8f2522b419779e7ea5f95c6cab17789ddf7c38c76b1485a2a0025d39e1dec22c:1", + "last_update": 1710794569, + "capacity": "7500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710779196, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710794569, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0215a106e71380f6b273e8232915f5fb3942822b1f8728d8d3ab8c9dc6eb2e8b02", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "888503251937067009", + "chan_point": "0e25169b920fe5598e0d6d886f27cc94ad3ebbfb0ab33c1a4cc26358fb597be4:1", + "last_update": 1710807858, + "capacity": "7500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710806196, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710807858, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "0215a106e71380f6b273e8232915f5fb3942822b1f8728d8d3ab8c9dc6eb2e8b02", + "channel_id": "888545033254010881", + "chan_point": "67a90c8dd80d368cf387389d8283efa81f9bccc97980ca1b435158beb8035e53:1", + "last_update": 1710782832, + "capacity": "7500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710780996, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710782832, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "channel_id": "888591212859424774", + "chan_point": "bf7474636c1048baa26ef72aa51b0b9626f8755efe4a2df2c8f7bcbeb4a074bb:6", + "last_update": 1710803684, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "125", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710803032, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710803684, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "channel_id": "888600008882126851", + "chan_point": "ba49acbcba48ccb1f9543fe0ec07a8dcf8ce29a62764bd46e1413d7932511d85:3", + "last_update": 1710855115, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1782000000", + "last_update": 1710855115, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710855115, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "channel_id": "888868289736212481", + "chan_point": "8026fb66e5c7d087a1f5c505c0f7163e258e0b44c8d67cf91a9ee11fc6844722:1", + "last_update": 1710803213, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710786333, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "700", + "disabled": false, + "max_htlc_msat": "9000000000", + "last_update": 1710803213, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "888918867285508096", + "chan_point": "222b58df9ce514d033994fd520f8d94b4403421ee97a644d5c88a20be1cdf5ff:0", + "last_update": 1710788428, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710788428, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710502897, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03fe320dd1a656842bf4cf31e25fbe76f18ae1d3ac35c477e32063b360024fd956", + "channel_id": "888921066311188480", + "chan_point": "2edf357094378b3e52a4ba0e987ebcadf3477b15d38a1f9f745f739e61ff1f9a:0", + "last_update": 1710804504, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710798790, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710804504, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "0315e18ba80891ff652bddd442764a354973b963e4ed4d1e89ea0adbcd4df2008b", + "channel_id": "888925464317853705", + "chan_point": "8f9d7a2d95293c013b3684ae3d23bf953ff68de267e45d26722e6b12e61d38a5:9", + "last_update": 1710804189, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710801697, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710804189, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03a21a8756ffc55dd1d83a02b0cc2e163f8d041e3f56b309232e5389c66c00364c", + "channel_id": "888974942422827012", + "chan_point": "9d52945a0a59466d10a64ab77ed70c9a0f05d037f63f714739ff6502e894ae86:4", + "last_update": 1710795189, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710795189, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790560, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "node2_pub": "03d4809d1a129228b9afb54dd5b3bcc29b8e59d5c8fc1f3104315baf19c8e0c76b", + "channel_id": "889106883778707456", + "chan_point": "571fde9db1eb61bc1ecb405db7d135910cd6a95a2def4db4618815988c4ec0ce:0", + "last_update": 1710837063, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "1980000000", + "last_update": 1710837063, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "180", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710803695, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "889200342210969600", + "chan_point": "afaa4a25d07946a47d64f351496f18a7d8fa2ebd30e813ee24e428e1a285ade4:0", + "last_update": 1709978556, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1709448931, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1709978556, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02404d14a1a5ffdf7f02a04b1aea3941a9ff3184b9ef2fd2c78af862cde9572188", + "node2_pub": "028a7930a96f7a604014a8d6874004262ea64aea4def90839c265f8c7ff8a4b34c", + "channel_id": "889225631013142529", + "chan_point": "48bc5386312bf4a937042325de22ccf309d06397a7dd34a9ac08d2b1138d86e0:1", + "last_update": 1710851082, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "27", + "disabled": false, + "max_htlc_msat": "980100000", + "last_update": 1710790261, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710851082, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "022c6a25c6090a64b2710ef7f15740c4a000c480ef16a3b5845864af448cb322ba", + "node2_pub": "028a7930a96f7a604014a8d6874004262ea64aea4def90839c265f8c7ff8a4b34c", + "channel_id": "889306994934349825", + "chan_point": "302028cc66b9a0a1d0164d57094abbe74ea7ef680e270cfb0daaf4edc93ef381:1", + "last_update": 1710771882, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "150", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710771565, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710771882, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "889569778255462400", + "chan_point": "a96e595d644013688f2408c99f6b40ff6ddc60dade8456628573f41c7a7ef156:0", + "last_update": 1710839832, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "602", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710803683, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710839832, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "channel_id": "889698420993490945", + "chan_point": "8e14ad183c9023ac39cab27dd6ad7c5f873e53f66aa9a89015cb4899e87d9fca:1", + "last_update": 1710829389, + "capacity": "1985495", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1965641000", + "last_update": 1710790420, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1965641000", + "last_update": 1710829389, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032cc4541b25e86e39a7d450a979c1a9adbe2878df3a93fcb59c96c700bfe26aa3", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "889698420993622017", + "chan_point": "32a86a229314f2786aed224268b5df1fb61cc13c1dd5dd20708603886c6ea813:1", + "last_update": 1710792863, + "capacity": "1992011", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1972091000", + "last_update": 1710786820, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "700", + "disabled": false, + "max_htlc_msat": "1972091000", + "last_update": 1710792863, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "node2_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "channel_id": "889809471625953281", + "chan_point": "3e555b10e98f49e349b991ef9597b3180c474d3fbfe13ae3c16d5804888eeefd:1", + "last_update": 1710844918, + "capacity": "12918498", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1000", + "fee_base_msat": "5353", + "fee_rate_milli_msat": "4035", + "disabled": false, + "max_htlc_msat": "12789314000", + "last_update": 1710844918, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "12918498000", + "last_update": 1710836831, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "889825964323700737", + "chan_point": "8f10761d38bc3c0e30cbd8b42118cc0e152cf1f7b57384bfb92dc8980e09119c:1", + "last_update": 1710785329, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "1121", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710785329, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710754081, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "889855651147612169", + "chan_point": "7ce4049dacc96cbed13afd0664d05d6d4f68ec7b6d1bd6181fe03736af188fea:9", + "last_update": 1710813189, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710813189, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790753, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "channel_id": "889855651147612176", + "chan_point": "7ce4049dacc96cbed13afd0664d05d6d4f68ec7b6d1bd6181fe03736af188fea:16", + "last_update": 1710795190, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710795190, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710792313, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "channel_id": "890019478399877121", + "chan_point": "98c43b13d184bb3e4f729e03f477d12c0f04af9c979f5b905371ab330b705c1a:1", + "last_update": 1710845484, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710845484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "55", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710700501, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "890105240352456705", + "chan_point": "549dee2681643c82424ca3917a8612a8299d378d515d6389c6f0c2a0cbc1e54e:1", + "last_update": 1710838854, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "10000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "74", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710776329, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710838854, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "036fe0795980588a1489d2ddf11f47a0ac36671e2cbff46bc4ace52738f15a1956", + "node2_pub": "022c6a25c6090a64b2710ef7f15740c4a000c480ef16a3b5845864af448cb322ba", + "channel_id": "890283361157185537", + "chan_point": "74dbaf90b518e0fab1b2fd7360ec9c584438e422ed0beefcb82f2f0727c5ebfd:1", + "last_update": 1710778765, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "150", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710778765, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710771715, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "890316346463027201", + "chan_point": "ee471cdfcd778ba38848b2d5c6e7dabc4cdb9a280ecda1eacccbd76b8df77f3e:1", + "last_update": 1710825908, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "340", + "disabled": false, + "max_htlc_msat": "1188000000", + "last_update": 1710825908, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710756696, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "890336137740222464", + "chan_point": "6f267a3a120c625b7dce07faf19b647ec53777a4a297f69f704861371b09c2f6:0", + "last_update": 1710024268, + "capacity": "76000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1200", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710024268, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1709834584, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "890507661605732359", + "chan_point": "b911aa7d9350b8891554835262a7856fc2bf670cebc2463d6c5141892a74e164:7", + "last_update": 1710838363, + "capacity": "16000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "15840000000", + "last_update": 1710788281, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "15840000000", + "last_update": 1710838363, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "890507661610319873", + "chan_point": "5ed0d6486be9fd4241fe4b7c44070cdfda10570c06857709f6501f6b30246e6d:1", + "last_update": 1710831319, + "capacity": "16000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "15840000000", + "last_update": 1710788281, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "15840000000", + "last_update": 1710831319, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d8853c04b69ce55c93d574bbb614e31f2654fb2f4049483774792cfd8d9af732", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "890521955192995845", + "chan_point": "e79bcf5ea0a02a36940c60c33c8c1846a652a9ef7ed2781975d1fbcf07e47aad:5", + "last_update": 1710847556, + "capacity": "16000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "15840000000", + "last_update": 1710847556, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "15840000000", + "last_update": 1710788281, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "890537348345036803", + "chan_point": "d4137b4b3afabee85f2ee2b23ce074ca807bca0c584ebf18526e1af2c2aaabc7:3", + "last_update": 1710847514, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "1000", + "last_update": 1710818804, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710847514, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "890817723814248449", + "chan_point": "183644bebe6807d9960f5a032a4ce87f8363f8a796efea39285f3c495e6cf892:1", + "last_update": 1710853964, + "capacity": "25100000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1708", + "disabled": false, + "max_htlc_msat": "24849000000", + "last_update": 1710853964, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "24849000000", + "last_update": 1710808030, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "channel_id": "890834216531001350", + "chan_point": "789018c2cbfaf1b21a41991bb147f9866f7c7c8605235c990f4950685409823e:6", + "last_update": 1710816510, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710816510, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "9", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710765004, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "030a425f5c69a29db30f6740d4e7df8f5612ef9955078ef4497490015464733dc8", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "890918878931189760", + "chan_point": "1d0e4c810323cf04490a6a977ebcfb0b8efdea5cdc6aeab9d79e524dc4f42a8a:0", + "last_update": 1710816263, + "capacity": "300000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "100", + "fee_base_msat": "0", + "fee_rate_milli_msat": "250", + "disabled": false, + "max_htlc_msat": "105000", + "last_update": 1710788861, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "700", + "disabled": false, + "max_htlc_msat": "297000000", + "last_update": 1710816263, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "890970556045983746", + "chan_point": "0346e8a51bc2e61160e02f2191c9dd3d5f15dca08d30a933b69fad8db8ccd37b:2", + "last_update": 1710788281, + "capacity": "88888888", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "1000", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "88000000000", + "last_update": 1710755091, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "88000000000", + "last_update": 1710788281, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "890970556045983759", + "chan_point": "0346e8a51bc2e61160e02f2191c9dd3d5f15dca08d30a933b69fad8db8ccd37b:15", + "last_update": 1710802429, + "capacity": "88888888", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710799081, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "400000000", + "last_update": 1710802429, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "890970556046114830", + "chan_point": "abb7dcbdf515266a788b6acfd636cf18b88bf847de190e9b966334ce6f3cc241:14", + "last_update": 1710848353, + "capacity": "88888888", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "88000000000", + "last_update": 1710790081, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "88000000000", + "last_update": 1710848353, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "890998043747811342", + "chan_point": "6a82f93ea79d3786b71052e86bd96a41c6fc94e44f37dc32de6af24572564c15:14", + "last_update": 1710838535, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710838535, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710785048, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "890998043747811345", + "chan_point": "6a82f93ea79d3786b71052e86bd96a41c6fc94e44f37dc32de6af24572564c15:17", + "last_update": 1710805241, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2169420", + "last_update": 1710805241, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710777848, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "891011237861851137", + "chan_point": "59d5749ec7c46ce5f6913e3893ba04c600db0efb05be79fda5cbacbbbf071d5d:1", + "last_update": 1710844729, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "662", + "disabled": false, + "max_htlc_msat": "4500000000", + "last_update": 1710844729, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "4500000000", + "last_update": 1710302904, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891086004748681217", + "chan_point": "dcf80c10558dcbc8aaafd325bff0b8c2e847e36ee3f77a729121eaaaf3e2c52d:1", + "last_update": 1710842281, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710842143, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710842281, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891086004748681221", + "chan_point": "dcf80c10558dcbc8aaafd325bff0b8c2e847e36ee3f77a729121eaaaf3e2c52d:5", + "last_update": 1710803683, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1500", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710776981, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710803683, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891087104157220867", + "chan_point": "80656ab60c3125d0c2330841e0e579d52546aa3c51e5e38a0b900aba4bbb36eb:3", + "last_update": 1710829389, + "capacity": "22222222", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710790081, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710829389, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "891088203768856582", + "chan_point": "2944e0642d5f48923d1b2975b6ecfbe57d53a741cdb477dee8041fd0dfd9a660:6", + "last_update": 1710820457, + "capacity": "22222222", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710820457, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710806281, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891088203768856583", + "chan_point": "2944e0642d5f48923d1b2975b6ecfbe57d53a741cdb477dee8041fd0dfd9a660:7", + "last_update": 1710840850, + "capacity": "22222222", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710790081, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710840850, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891088203768856585", + "chan_point": "2944e0642d5f48923d1b2975b6ecfbe57d53a741cdb477dee8041fd0dfd9a660:9", + "last_update": 1710843632, + "capacity": "22222222", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1841", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710843632, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "22000000000", + "last_update": 1710804481, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "891147577330434057", + "chan_point": "326b7165f8a8bf4cdfc713c6b52e295642a9d28bd1424a2d1675ff9ac4590dad:9", + "last_update": 1710827881, + "capacity": "35024288", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000000", + "fee_base_msat": "5500", + "fee_rate_milli_msat": "3714", + "disabled": false, + "max_htlc_msat": "34674046000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "34674046000", + "last_update": 1710827881, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "891176164630134785", + "chan_point": "1e85a3afa255ec21c92522be49ce314600a57a26a69b4a3fa18cba4d2a9245e4:1", + "last_update": 1710831421, + "capacity": "20100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "19899000000", + "last_update": 1710831421, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "826", + "disabled": false, + "max_htlc_msat": "19899000000", + "last_update": 1710831032, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891176164675026951", + "chan_point": "6b374e693fcc44e54167b958a70568c16dc6a7c01d4f3e03de58ed5caf622fb5:7", + "last_update": 1710838131, + "capacity": "16829112", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "16660821000", + "last_update": 1710838131, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "16660821000", + "last_update": 1710790081, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891176164675158027", + "chan_point": "6466c59736d408e10a11e2127710bd94b025f148f35a2db6ba4069b19af89a23:11", + "last_update": 1710799598, + "capacity": "186058085", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1499", + "disabled": false, + "max_htlc_msat": "184197505000", + "last_update": 1710783358, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "184197505000", + "last_update": 1710799598, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891176164675223563", + "chan_point": "be9c2008ad15ab4dbfe34aab8f9829410f7a342479416d2715d1eba5feb08145:11", + "last_update": 1710831430, + "capacity": "333333333", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "330000000000", + "last_update": 1710831430, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "330000000000", + "last_update": 1710790081, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891181662246666250", + "chan_point": "7219175edcc8d63018d094e6c3e45c5edfb0bb889207ede2306514878cb2e968:10", + "last_update": 1710790081, + "capacity": "46094394", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "1000", + "last_update": 1710730652, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "45633451000", + "last_update": 1710790081, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "891297110893592577", + "chan_point": "a98fd8d47d454a0bbef55468f69cc03dc22c55506eee1a569c4773f6b4150631:1", + "last_update": 1710806758, + "capacity": "25100000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "322", + "disabled": false, + "max_htlc_msat": "24849000000", + "last_update": 1710800551, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1249", + "disabled": false, + "max_htlc_msat": "24849000000", + "last_update": 1710806758, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891310305098596356", + "chan_point": "888876aa8768df1c36ab32aea53fee372a9d661c297f2c0f282378027e51466f:4", + "last_update": 1710842454, + "capacity": "400000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "396000000000", + "last_update": 1710838681, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "396000000000", + "last_update": 1710842454, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "028dcb644197ce997574bd24360df94d3f5cd7926caae51eded371e2926979ed19", + "channel_id": "891772099941040129", + "chan_point": "afbe18facda34e8727ef64d6bf85aa2560da91b85020307fcd7e72048508f441:1", + "last_update": 1710814922, + "capacity": "6000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710814358, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710814922, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "891774298923008001", + "chan_point": "786d85b944a16dbd21282eb26a229114bd3c4e6c187e8a5ed6d753be96d18c52:1", + "last_update": 1710856681, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710792825, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710856681, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "030a425f5c69a29db30f6740d4e7df8f5612ef9955078ef4497490015464733dc8", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "891803985800986625", + "chan_point": "5dc3a53fe8fc448c6fe9fb589e785173d1f2ae5148bc45de8b322265604fe950:1", + "last_update": 1710816225, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710816225, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "100", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2250", + "disabled": false, + "max_htlc_msat": "10000", + "last_update": 1710769273, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "891820478477369344", + "chan_point": "d78c7fa0571fce5b5923bdcffbceeece4782fb9df762ab08651ea1c64becf797:0", + "last_update": 1710856389, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710791025, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710856389, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "channel_id": "892074465645494273", + "chan_point": "b50e6b5ed9ee90291dfdf981a3b67e2d6548c9bc745ad946130340cd2b44d26b:1", + "last_update": 1710643142, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1709649248, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1499", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710643142, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "channel_id": "892074465645494281", + "chan_point": "b50e6b5ed9ee90291dfdf981a3b67e2d6548c9bc745ad946130340cd2b44d26b:9", + "last_update": 1710801136, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710801136, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "299", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710801136, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "892175620656660482", + "chan_point": "a4cd35df1aa434f71a09a6a084324faae4d60bffff01cce83130c917457e3808:2", + "last_update": 1710854911, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "5352", + "fee_rate_milli_msat": "3345", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710854911, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "16777215000", + "last_update": 1710775854, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "892364736701595649", + "chan_point": "073878b3efb96104ff7a0740f2e72d61774578740b491b9d3e3d6d9a97f59471:1", + "last_update": 1710836574, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "16777215000", + "last_update": 1710836574, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "5880", + "fee_rate_milli_msat": "2940", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710807742, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "892391124966703104", + "chan_point": "99042e998da35954718904204837325d73faa49e0956987b407e3372ae85d9f5:0", + "last_update": 1710807743, + "capacity": "5417500", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "51898", + "fee_rate_milli_msat": "5242", + "disabled": false, + "max_htlc_msat": "5363325000", + "last_update": 1710807743, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "5417500000", + "last_update": 1710795190, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "892476886835920896", + "chan_point": "d2be799b55f95db7bd69a952cb50926d35664293b4ab562b2dbb8fa8b6ce7c4f:0", + "last_update": 1710850635, + "capacity": "16416666", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "16416666000", + "last_update": 1710775535, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1702", + "fee_rate_milli_msat": "919", + "disabled": false, + "max_htlc_msat": "16252500000", + "last_update": 1710850635, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "channel_id": "892613226335764480", + "chan_point": "412185e9d19aea85e45d9ab0ae7e4fe307e2d9f40cc88cdd0c0965bbddb18deb:0", + "last_update": 1710851758, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "305", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710836574, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "799", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710851758, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "channel_id": "892613226335764485", + "chan_point": "412185e9d19aea85e45d9ab0ae7e4fe307e2d9f40cc88cdd0c0965bbddb18deb:5", + "last_update": 1710849850, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "599", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710756358, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710849850, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02404d14a1a5ffdf7f02a04b1aea3941a9ff3184b9ef2fd2c78af862cde9572188", + "node2_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "channel_id": "892623121899978752", + "chan_point": "8e71461f277563ecca33e8aeea08b93f09051aadd038922a90e5eb3d7c815478:0", + "last_update": 1710840661, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "23", + "disabled": false, + "max_htlc_msat": "1960200000", + "last_update": 1710840661, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710821050, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "892839725692485633", + "chan_point": "6e06d79c8d3819474254f598539bcd1cb58725e895d4d9392298a6ca8398e416:1", + "last_update": 1710849481, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710836574, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710849481, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "node2_pub": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "channel_id": "892878208667025408", + "chan_point": "09c3a29e4d743f0be5460529760a6a62488ace254f89d537f6ab0e73312c6f65:0", + "last_update": 1710855079, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710855079, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": true, + "max_htlc_msat": "1980000000", + "last_update": 1710743539, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "028dcb644197ce997574bd24360df94d3f5cd7926caae51eded371e2926979ed19", + "channel_id": "892958473079488513", + "chan_point": "ee5c98899edd2f39e69c1f09fdf01f677f113c873e27c67cc3826604d9fffb87:1", + "last_update": 1710799753, + "capacity": "2950000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "2920500000", + "last_update": 1710780722, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "2920500000", + "last_update": 1710799753, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "892984861294002177", + "chan_point": "e2a0450a2b7ee14fd9ec0a186ed5b46c55a3e302c95c96d0e15ca8bc26946df7:1", + "last_update": 1710820354, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "520", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710820354, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710756696, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "893122300194717699", + "chan_point": "d0e7662192eee27e3340207d040b1345cef8ac14704525207bda2acc83adf399:3", + "last_update": 1710848329, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710848329, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710839931, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03fe320dd1a656842bf4cf31e25fbe76f18ae1d3ac35c477e32063b360024fd956", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "893179474859065345", + "chan_point": "c1a93914af48ad632774f427cffe8aeaec3023d0318c4dac54b75e270ab4171f:1", + "last_update": 1710811949, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "5000", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710800284, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710811949, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "893215758799798273", + "chan_point": "cfa8650babe493c66405774a745a911aff86656ef151c20185225ced026ec8e4:1", + "last_update": 1710856106, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710856106, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710756786, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "893215758837481473", + "chan_point": "66237ef935ebf01a06db6e782faa79208e26aedf0afea726858d3f04d7a86fa1:1", + "last_update": 1710778425, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710778425, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710769269, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "893283928513904641", + "chan_point": "ced9802a90b8d23d837295caad8133e13000d3c68d456e90d1a3a94d1f8f296c:1", + "last_update": 1710837891, + "capacity": "27044530", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "26774085000", + "last_update": 1710837891, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "26774085000", + "last_update": 1710835344, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "channel_id": "893321311815008257", + "chan_point": "a8129b2d4cb6a82bffa0cea0c710db9ecc512bdddf6852cc129b6914a9ee2b7d:1", + "last_update": 1710853254, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710838543, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710853254, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "029a0d058126445a179586a104a8425bc996b88b0bfc533eed34f49d34b1c3b684", + "channel_id": "893526920477802497", + "chan_point": "94aad85fc31c03bfa97d829f86ddeae70a6754532fd67bfdb24b853b39b45c2d:1", + "last_update": 1710794571, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710794571, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 450, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710776347, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "036fe0795980588a1489d2ddf11f47a0ac36671e2cbff46bc4ace52738f15a1956", + "node2_pub": "029a0d058126445a179586a104a8425bc996b88b0bfc533eed34f49d34b1c3b684", + "channel_id": "893526920477802498", + "chan_point": "94aad85fc31c03bfa97d829f86ddeae70a6754532fd67bfdb24b853b39b45c2d:2", + "last_update": 1710778147, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 450, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710778147, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710775315, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "893537915591065600", + "chan_point": "d8ad7f1d03911c10ad7013666cb890d61ed06078e6772c771e4bb699d5ce24fc:0", + "last_update": 1710840771, + "capacity": "12928658", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "128", + "disabled": false, + "max_htlc_msat": "12799372000", + "last_update": 1710796523, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "517", + "disabled": false, + "max_htlc_msat": "12799372000", + "last_update": 1710840771, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bb086b83afa45795c2265ebc76dcc749a590bcd83ba244c27d4f47b1102ffc60", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "893599488252772352", + "chan_point": "ca6faddd6b7eeac3e73dba9b245641c994249cf2dc1cedfc4918fdcdfe3e441e:0", + "last_update": 1710827934, + "capacity": "1500000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "889", + "disabled": false, + "max_htlc_msat": "1485000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "1485000000", + "last_update": 1710827934, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bb086b83afa45795c2265ebc76dcc749a590bcd83ba244c27d4f47b1102ffc60", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "893600587721605121", + "chan_point": "58aee236193915286bb417185495b192a4e5128741302cf963466e8a43ca715b:1", + "last_update": 1710844134, + "capacity": "1500000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "10000000", + "last_update": 1710356804, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "675000000", + "last_update": 1710844134, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "893782007232331777", + "chan_point": "ca07b70fe79264ce94b7c21592b1be5643a13ef90fac011c8b75ee7c2f4ca03b:1", + "last_update": 1710820883, + "capacity": "80000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "4910000000", + "last_update": 1710820883, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "79200000000", + "last_update": 1710790254, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "893803997415079936", + "chan_point": "9751d6cec683f435b087e042b29ac009bdfb7f5538aeedda49d92764de4380f4:0", + "last_update": 1710810978, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "4566", + "fee_rate_milli_msat": "3653", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710810978, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "16777215000", + "last_update": 1710755830, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bb086b83afa45795c2265ebc76dcc749a590bcd83ba244c27d4f47b1102ffc60", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "893820490095919105", + "chan_point": "0ef0714101852b6ff7d2ed00f02b5fce8b72573488361fe64cd6b4d07227f71b:1", + "last_update": 1710833334, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710755830, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710833334, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "03a21a8756ffc55dd1d83a02b0cc2e163f8d041e3f56b309232e5389c66c00364c", + "channel_id": "893834783817859073", + "chan_point": "9d5956d9a475b7e93767489b12d4fcd7edadebef73337441e358b156fffe10a6:1", + "last_update": 1710856361, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "100000000", + "last_update": 1710856361, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4500000000", + "last_update": 1710844560, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "894117358259339267", + "chan_point": "ed3bf9ff6eca6522555a96d510f6976fc6badd87320fbbc1df8190aa4bb07ee5:3", + "last_update": 1710851231, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "1087", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710844216, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710851231, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "03d4809d1a129228b9afb54dd5b3bcc29b8e59d5c8fc1f3104315baf19c8e0c76b", + "channel_id": "894195423551356929", + "chan_point": "1caf42d9ecbe23f4791cb8c94119bd77b41d251edd409491a2d73c24a230fad5:1", + "last_update": 1710850637, + "capacity": "6044616", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "1644", + "fee_rate_milli_msat": "49", + "disabled": false, + "max_htlc_msat": "5984170000", + "last_update": 1710850637, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "180", + "disabled": false, + "max_htlc_msat": "6044616000", + "last_update": 1710776263, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "894195423551356930", + "chan_point": "1caf42d9ecbe23f4791cb8c94119bd77b41d251edd409491a2d73c24a230fad5:2", + "last_update": 1710837521, + "capacity": "16416666", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "357", + "fee_rate_milli_msat": "150", + "disabled": false, + "max_htlc_msat": "16252500000", + "last_update": 1710837521, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "16416666000", + "last_update": 1710776354, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "894388937633300480", + "chan_point": "e7b4402c33c52131e5ea4e0866e7f2a3c0c579403761972d6ee43d4d19982b22:0", + "last_update": 1710681712, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "0", + "fee_base_msat": "0", + "fee_rate_milli_msat": "954", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710595864, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "0", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1", + "last_update": 1710681712, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "894647322887716864", + "chan_point": "904c884e96788fc281949714f7c03fb30dda6c544295c367708b0ef3b49cd7c4:0", + "last_update": 1710837235, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710837235, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710822654, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "894650621489053696", + "chan_point": "0fc7f1e247d222dd3ee04cdcd793e6abc0613b968a8ffd211ee39c507726061b:0", + "last_update": 1710849335, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710849335, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710828002, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "894816647619280897", + "chan_point": "f3b565fc8b85693bc6f4fadd825e5a282f884aaac2c5423185434c3da7ee5095:1", + "last_update": 1710785048, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "1000", + "last_update": 1710730650, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710785048, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "894843035960147968", + "chan_point": "cfcfe8f1dc8cf0f20a6292facfc00d7e820e89cff3de64bd057801fc3ec3fc8d:0", + "last_update": 1710757535, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710757535, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710312917, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "894887016460189697", + "chan_point": "cf33839abd2cb02af6981fb58c16c9ccfda9876e6f258d050c05dc19c97bf979:1", + "last_update": 1710856363, + "capacity": "3500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "3465000000", + "last_update": 1710792827, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "3465000000", + "last_update": 1710856363, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "895058540187353088", + "chan_point": "e23020b018bea64b0a950b0c60686a6e78405eb62bfc11c143b5edf0dbd8ffcc:0", + "last_update": 1710845574, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "700", + "disabled": false, + "max_htlc_msat": "9000000000", + "last_update": 1710845574, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "200000000", + "last_update": 1710828479, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "channel_id": "895437871736815616", + "chan_point": "3a40021f9958722258455c2e5499124242786eeee93f1a42d236ceed36e861f0:0", + "last_update": 1710731035, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710731035, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1709724541, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "895630286316109825", + "chan_point": "ed798db81091601e6d3eaa5b3439e654d6d341dcebb1b0ef592fa204e20caf0a:1", + "last_update": 1710845971, + "capacity": "8000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "8000000000", + "last_update": 1710775543, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "30", + "fee_rate_milli_msat": "31", + "disabled": false, + "max_htlc_msat": "7920000000", + "last_update": 1710845971, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "02ab80cce7e7dff00c964e4b2006eb4c8af4e71187572156a88275d3c126b90b66", + "channel_id": "896055797301116951", + "chan_point": "06643d87bd247596ecf2e34130542cceba2d5075c5a2bbbb8ca86156d5dacfc0:23", + "last_update": 1710856039, + "capacity": "4500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "250", + "disabled": false, + "max_htlc_msat": "4455000000", + "last_update": 1707686941, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "278", + "disabled": true, + "max_htlc_msat": "4455000000", + "last_update": 1710856039, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "channel_id": "896650633019129857", + "chan_point": "ff46179b5b533306e297b3d962ba256462d913e0ee097e4538e829b94c7aeb59:1", + "last_update": 1710822835, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710781030, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1550", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710822835, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "896672623394095105", + "chan_point": "0b0a82d6fbb784a009e6a19c9de1a8b0f67bf4928aec3923b0fe7b04b54abaa1:1", + "last_update": 1710853267, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "60", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710783988, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "2190000000", + "last_update": 1710853267, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "896692414404231169", + "chan_point": "28bad9c4d16c70e5beecaa00eb8eceb50a5629c73b3eefe4b404cdc6b2d462fc:1", + "last_update": 1710825484, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "3419", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "900", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710777682, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "897487361531641857", + "chan_point": "5c8f5688296b403ac44bf5fc47e69071a606d1b1045b0f46a172d89458ebd2ae:1", + "last_update": 1710825484, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1090", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710825484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710320815, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "028dcb644197ce997574bd24360df94d3f5cd7926caae51eded371e2926979ed19", + "channel_id": "897503853995032577", + "chan_point": "fb87ed329859a4e5803ed742ac43130d91ffd20eff70b1f266fb8781c68664b8:1", + "last_update": 1710757322, + "capacity": "2500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "1125000000", + "last_update": 1710757322, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "1125000000", + "last_update": 1710064588, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "03ca2ce661ceb09c245588b8f4cb5dd041e387dca7d33ff3754eeb3928553aac98", + "channel_id": "897846901912305665", + "chan_point": "d484798b7859376b67a07c58b67d57f7e7262858304d80a203b5dd2fa09b7ae9:1", + "last_update": 1710846093, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "157", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710846093, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710778840, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "0289b70b52a04e951446674bedb571336295a2890ca0079639ebc067076277d571", + "channel_id": "898362572660342784", + "chan_point": "323c7d26cf44221c29e177b3cb04061cdc479b0e307dda0e65c89f1e09050505:0", + "last_update": 1710856517, + "capacity": "51764750", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "51247103000", + "last_update": 1710856516, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "51247103000", + "last_update": 1710856517, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "898938716679176192", + "chan_point": "1edf84a0e977ed957b1d66f74192fdb81f857cedb3b08bf33ea6670007a604c4:0", + "last_update": 1710853283, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710777835, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "547", + "disabled": false, + "max_htlc_msat": "6270000000", + "last_update": 1710853283, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "channel_id": "899076155690909697", + "chan_point": "4b78e9775001132a318ff64ca4536ca7c52fdba585d45b6bb7a9a5b843ded665:1", + "last_update": 1710823639, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710822835, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "220", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710823639, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "899683086217183233", + "chan_point": "8695cd7ec166dd9c1b720a4d25df16f0047b869f69ecdf51cf913c8fe238b3d7:1", + "last_update": 1710825485, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "110", + "disabled": false, + "max_htlc_msat": "16777215000", + "last_update": 1710825485, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "29479", + "fee_rate_milli_msat": "14739", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710817271, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "899683086217183236", + "chan_point": "8695cd7ec166dd9c1b720a4d25df16f0047b869f69ecdf51cf913c8fe238b3d7:4", + "last_update": 1710856264, + "capacity": "12625395", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "100", + "fee_base_msat": "2", + "fee_rate_milli_msat": "158", + "disabled": false, + "max_htlc_msat": "12499142000", + "last_update": 1710854910, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "413", + "disabled": false, + "max_htlc_msat": "12625395000", + "last_update": 1710856264, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021d2436cab847373a4212bf6d754ead5304f5d0791479643893a837b295f3441c", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "899802932799995905", + "chan_point": "87dbbc3dc88280f0c100593130075caca22ce2e357b2eb4d577c88ef3a6af572:1", + "last_update": 1710854909, + "capacity": "5442253", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "28", + "disabled": false, + "max_htlc_msat": "5442253000", + "last_update": 1710786986, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "988", + "disabled": false, + "max_htlc_msat": "5387831000", + "last_update": 1710854909, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "899934874331316224", + "chan_point": "c29809316b6dbb9947f719a1a1cadcd41ff483a2bce0f5bb4335f89eaa2141b1:0", + "last_update": 1710842221, + "capacity": "8149610", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "8149610000", + "last_update": 1710842221, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "3470", + "fee_rate_milli_msat": "4626", + "disabled": false, + "max_htlc_msat": "8068114000", + "last_update": 1710810977, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03db3386adf0e2221555a7f3fc7282eca60c5eabfed8e1789d591a9241c342c091", + "node2_pub": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "channel_id": "900579188194344961", + "chan_point": "fbf5726e17d4bbf99c6cd0277d0a91aecb3f46936d1c0677a5a1d5a6834e27ef:1", + "last_update": 1710719763, + "capacity": "50000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": true, + "max_htlc_msat": "49500000", + "last_update": 1710719763, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "49500000", + "last_update": 1701327976, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "node2_pub": "022b0de0e9156024d3f1ba54b3602f21eae93c4087a552140d25805175d3d9a1d3", + "channel_id": "900586884660658177", + "chan_point": "18d1cd672fca25bdee14d9c8dcd5c1bd3cb9aeaaa4996e9fa4afa1720ae33c88:1", + "last_update": 1710855388, + "capacity": "360000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "356400000", + "last_update": 1710855387, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "356400000", + "last_update": 1710855388, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "channel_id": "900623168648970241", + "chan_point": "63b7c4332fbba23e2a094ad38ff9df10266bfa881dca949577e21a1bc520067c:1", + "last_update": 1710851954, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710846235, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710851954, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03c41fde73d7853d07c0b0baf110e91c5f54f768cc94d58ddc0d3e947fa7d27e29", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "900653954925920257", + "chan_point": "a9a2f5c029db95b5f66d5f73ec3db47e05dcdd332f4da4d1552bd2a4a67ca9fc:1", + "last_update": 1710807743, + "capacity": "12312489", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "0", + "fee_base_msat": "0", + "fee_rate_milli_msat": "32", + "disabled": false, + "max_htlc_msat": "12189365000", + "last_update": 1710807743, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "0", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "12189365000", + "last_update": 1710254954, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "900784796753264641", + "chan_point": "22b4d8ea9c88febed493587429513aa35fe3f5f0f159aa366c0fbabe6eab203e:1", + "last_update": 1710857048, + "capacity": "7500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710782025, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710857048, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "900911240727166976", + "chan_point": "648897d747d6462bb815f84d1b0cf8b9ec87cb9aa76f806a55cc40ea165fab3d:0", + "last_update": 1710837520, + "capacity": "15712344", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "787", + "fee_rate_milli_msat": "444", + "disabled": false, + "max_htlc_msat": "15555221000", + "last_update": 1710837520, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "15712344000", + "last_update": 1710256226, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "901597335843504129", + "chan_point": "79e26ede939fb09edc4e2df45c4f26a517a7ee188bf336b392e88944468cc9b7:1", + "last_update": 1710855809, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710843531, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "599", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710855809, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "node2_pub": "028dcb644197ce997574bd24360df94d3f5cd7926caae51eded371e2926979ed19", + "channel_id": "902036040901853185", + "chan_point": "dc0e8ea7a3d9ff7252a26c72fcff828f105e970584c08c29f7216d6d49f22cdb:1", + "last_update": 1710797734, + "capacity": "2500000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "80", + "disabled": false, + "max_htlc_msat": "2475000000", + "last_update": 1710796922, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "2475000000", + "last_update": 1710797734, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02e98e5929c25f16f7c15b9026b9986e32dc36ba62c6497f436984b40fc3f0d7ac", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "902083319902568448", + "chan_point": "91c822587c576e11376d1a0db73bacf277502ec90fb7ef33a519aa40239fa62b:0", + "last_update": 1710834238, + "capacity": "250000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "247500000", + "last_update": 1710833564, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "504", + "disabled": false, + "max_htlc_msat": "247500000", + "last_update": 1710834238, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "902437362674958337", + "chan_point": "e7e8ca1cd9b79a6cc5b93b5b28e8cb5129e0e31f24987a5da588ccc2b1554e70:1", + "last_update": 1710833133, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "80", + "disabled": false, + "max_htlc_msat": "3465000000", + "last_update": 1710833133, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "28", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710756698, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "channel_id": "902454955058790401", + "chan_point": "8de56f42ca25fe06c55819ce7a5d08087ef208d218513bd50007171bfa72fb75:1", + "last_update": 1710849343, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710849343, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "299", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710756358, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "node2_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "channel_id": "902555010536505345", + "chan_point": "6961497cb9f493eded56bf4c9366f49dbf4883de337987d3dcb450d6408d1bb0:1", + "last_update": 1710827331, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710827113, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710827331, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "902555010536570880", + "chan_point": "11ce565b1f6d471da3f54d9a6165213734c49c476b7aceb71595c26438ede675:0", + "last_update": 1710853279, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710845484, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "288", + "disabled": false, + "max_htlc_msat": "4020000000", + "last_update": 1710853279, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "903624835328049152", + "chan_point": "1c22101dd1011f942afb18f3b01b1611f44ced4a329b5b8993e2c90ce869c543:0", + "last_update": 1710850348, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710850348, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "252", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710843238, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "903624835440967680", + "chan_point": "e308cc0ab7eb0c9dda103ca49f2186acfc3239809c27d25725f3d1eb2870b6c7:0", + "last_update": 1710795190, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "756", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710789238, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710795190, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "02df5ffe895c778e10f7742a6c5b8a0cefbe9465df58b92fadeb883752c8107c8f", + "channel_id": "903775468383240193", + "chan_point": "340be371ec84dbe5025805906b0a5f28732d79911d12a7d56f7aa708d1855fb5:1", + "last_update": 1710812213, + "capacity": "4000000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "3960000000", + "last_update": 1710636569, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1111", + "disabled": false, + "max_htlc_msat": "3777777000", + "last_update": 1710812213, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "channel_id": "904165795090530308", + "chan_point": "1401ebf5b6dc3ab27932bcaa9a185bb0b039b6d523b9d68984c1234c399e7e0f:4", + "last_update": 1710839035, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1248", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710819358, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710839035, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "904165795090530313", + "chan_point": "1401ebf5b6dc3ab27932bcaa9a185bb0b039b6d523b9d68984c1234c399e7e0f:9", + "last_update": 1710838700, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "602", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710838700, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "750", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710803035, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "904165795090530315", + "chan_point": "1401ebf5b6dc3ab27932bcaa9a185bb0b039b6d523b9d68984c1234c399e7e0f:11", + "last_update": 1710848122, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1600", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710810235, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "73", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710848122, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "channel_id": "904192183207723009", + "chan_point": "59332efb90e365807de7d491a05855029c8adb7d0060b124cff19bcfdbc10254:1", + "last_update": 1710855413, + "capacity": "10101010", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "598", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710853558, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "250", + "disabled": false, + "max_htlc_msat": "9000000000", + "last_update": 1710855413, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "03011e480a671ac71dbc3fc3e8fe0db32bb9a10cc4d824663e09557d446ef80679", + "channel_id": "904362607750152193", + "chan_point": "de3570cdf2cb8978730951a6a474cefa32d0fc2245d731d5477920f3d07899cb:1", + "last_update": 1710849032, + "capacity": "80000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "79200000000", + "last_update": 1710848548, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2002", + "disabled": false, + "max_htlc_msat": "79200000000", + "last_update": 1710849032, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "904379100430532609", + "chan_point": "6e2ab5573253b612d91f0f5a957af770f5bbed188c51f3f754b0b22c0e89b35a:1", + "last_update": 1710827743, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "334", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710827743, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710825485, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "904553922785443840", + "chan_point": "281459d204deabb7a23b8f5d2ee09ba38df3e47ebe2e1653e809c51eaaf75909:0", + "last_update": 1710851758, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "504", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710787438, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2199", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710851758, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "904646281748021248", + "chan_point": "8f3d58ae324262465bdfa1e1d4391c1ebad7e7d7576f3c27e1f228f34558ee1b:0", + "last_update": 1710791779, + "capacity": "16777215", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "0", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1143", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710791779, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "0", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "16609443000", + "last_update": 1710254915, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "904813407494864896", + "chan_point": "676fb7fee2e2ed1bcd1565b42249111e900c7edbeee2e82cb241cbd3af484e82:0", + "last_update": 1710849481, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1550", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710783235, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710849481, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "904816705950580736", + "chan_point": "d5ced6c98cc691ca7ed652f1ae0f77fc14c7b1b05ab1a340ed354363f49b961f:0", + "last_update": 1710839638, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "252", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710839638, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710782331, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "905254311684341760", + "chan_point": "73ec5fef61ec16ba615daf596cf52305e9ed7e096c118f822eabf8616a059024:0", + "last_update": 1710827833, + "capacity": "75000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1197", + "disabled": false, + "max_htlc_msat": "74250000000", + "last_update": 1710783838, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "74250000000", + "last_update": 1710827833, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "905260908694208512", + "chan_point": "a24361793736b046f77a06b3b45f0d1695b766378a319b20f71fe7568e0b4162:0", + "last_update": 1710790081, + "capacity": "60000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "882", + "disabled": false, + "max_htlc_msat": "59400000000", + "last_update": 1710785638, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "59400000000", + "last_update": 1710790081, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "905312585750020097", + "chan_point": "4c39599e6789f6aca2c255f00982f434ce9a26bde1d687d2bbf97bc06349ca33:1", + "last_update": 1710851722, + "capacity": "6000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "103", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710851722, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "210", + "disabled": false, + "max_htlc_msat": "1000", + "last_update": 1710814013, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "905851346364071937", + "chan_point": "8efa558c5d4f9fd1e15214f59b0baf7a1635c47f436ec0011da9a15728b725da:1", + "last_update": 1710852912, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "1074", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710771021, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710852912, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "906005278028201985", + "chan_point": "d2d492a21d03428fe127976f75597cd9d62856c3c2603fff11dfc98725d768b2:1", + "last_update": 1710844832, + "capacity": "1500000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "516", + "disabled": false, + "max_htlc_msat": "675000000", + "last_update": 1710844832, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "675000000", + "last_update": 1710249572, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03a21a8756ffc55dd1d83a02b0cc2e163f8d041e3f56b309232e5389c66c00364c", + "channel_id": "906008576572784643", + "chan_point": "da862edd8d4edfe35c656cd6e1497e88d41af38bbc55a42cf215e4ade5c93546:3", + "last_update": 1710853558, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2199", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710853558, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710849960, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "906008576572784647", + "chan_point": "da862edd8d4edfe35c656cd6e1497e88d41af38bbc55a42cf215e4ade5c93546:7", + "last_update": 1710801681, + "capacity": "21000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710801681, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "99", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710780001, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "029ed7e456a53568e12f013173dc5be2a92c86302e15038d150362933b754e5d21", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "906089940340047872", + "chan_point": "c17197cb792f365d7a22fbb2e40d102fa72754b40fd35cc0e4a8827f8a117082:0", + "last_update": 1710855001, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": true, + "max_htlc_msat": "49500000", + "last_update": 1710855001, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710841067, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "906356022148268032", + "chan_point": "3e6fb02329691cc1ac41f9dabfd2adaff1de4d07552615386387d557bc418de7:0", + "last_update": 1710775110, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710775110, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710686087, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "node2_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "channel_id": "906413196839944193", + "chan_point": "971be419180492c9dc08dcb3b4b51e24b7072e8a36a676fd4873db66e1318ef1:1", + "last_update": 1710836831, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "2718", + "fee_rate_milli_msat": "924", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710835407, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710836831, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "channel_id": "906415395757752321", + "chan_point": "83afd531ec95bfaf4367604bbd71ff564b1168801fbf28df00980972456c3cbf:1", + "last_update": 1710845480, + "capacity": "21000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710845480, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 120, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1249", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710828358, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02b9735190f3e98ee3d96f0cef0bcd02af2ee37141d5c2d969683b354e2293fd8f", + "node2_pub": "0303e0423fd2c40d73978b03069f63d6ca5ae1abf61f5666bbbea886181f21e8e3", + "channel_id": "906699069780066305", + "chan_point": "563834d041662028eb0ae34fb98bdf9646d21de2b3aec3c486dfbb1bd6c4287b:1", + "last_update": 1710847222, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1436000000", + "last_update": 1710804929, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "6", + "disabled": false, + "max_htlc_msat": "3507225000", + "last_update": 1710847222, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "907088297038446592", + "chan_point": "6a5795e372ac56b4555dcc179a1f125d39f2f48800f079cd3ef8584cafae4817:0", + "last_update": 1710848353, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710774858, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710848353, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03518cfddeb18d50db01fc316984bac961a6d978115a52259cd5d2e47fbe363e94", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "907149869565607937", + "chan_point": "a5696d10831ddd16cb46253cb0ddd8ecaf8a2a8ed7517d78449ee09a00d7e533:1", + "last_update": 1710811646, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "450000000", + "last_update": 1710811646, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "1", + "last_update": 1710018886, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "node2_pub": "029ed7e456a53568e12f013173dc5be2a92c86302e15038d150362933b754e5d21", + "channel_id": "907167461862735872", + "chan_point": "b3125bff3e6b5448d162233e7569de581037fdebff94c78b0648108793084550:0", + "last_update": 1710827141, + "capacity": "15000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "125", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710827141, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "620", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710771238, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "908236186969309184", + "chan_point": "eedddbd82a167710a122bad1ce4229bbbd6fe5522e5e8448c0666eb68aec4a0f:0", + "last_update": 1710849850, + "capacity": "7000467", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "6930463000", + "last_update": 1710755091, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "6930463000", + "last_update": 1710849850, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "908240585088761857", + "chan_point": "62a6bfdf6f16889b5b72c63c3e3551c9b98e8ad5c60619b965b124b20ed69588:1", + "last_update": 1710854318, + "capacity": "4650000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "1", + "last_update": 1710072529, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "1000", + "last_update": 1710854318, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "channel_id": "908252679706509314", + "chan_point": "12632e2bc4ef20018a7c26a4b58ae94659dbd032f56f6b5df0a536fbc3a981d1:2", + "last_update": 1710842281, + "capacity": "200000000", + "node1_policy": { + "time_lock_delta": 18, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "198000000000", + "last_update": 1710842281, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 18, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "198000000000", + "last_update": 1710795848, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "908409910008217600", + "chan_point": "47e3870351dbacc24b26ec18d21afdc80cebf690573576a15e626335f5bb50ab:0", + "last_update": 1710788650, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1512", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710785638, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710788650, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "908552846380564480", + "chan_point": "d2fb0b1538d787f2b57b9ce48ccb0eb70098885506f4c06dc5c0107ab1968787:0", + "last_update": 1710783838, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1071", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710783838, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710748936, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "908655100992880641", + "chan_point": "0acfd2470db4eb06268b3f22b4c45c0fdcc11d10b6519577662e54823a0f264e:1", + "last_update": 1710841427, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "150", + "disabled": false, + "max_htlc_msat": "4500000000", + "last_update": 1710841427, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "4500000000", + "last_update": 1710835874, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "908677091317514240", + "chan_point": "60ea6bc0bf78c321439693d86d80a3b9eb9208f358941ab5595624e926d7b86e:0", + "last_update": 1710853799, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710772034, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "1", + "last_update": 1710853799, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03501a74753e0f6ae270a1e4e2ffbbc37f7a796360e650c1121c18e116b22ac106", + "channel_id": "908689185928642561", + "chan_point": "6f6e4a04e9fab0e5d24c77e06b80a0a0e1cc6600a802b00d839ae25d1bdef1e8:1", + "last_update": 1710844254, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710839050, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710844254, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03c41fde73d7853d07c0b0baf110e91c5f54f768cc94d58ddc0d3e947fa7d27e29", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "908715574232940544", + "chan_point": "681d6807b9e787832e69ce657201e3c61d3b7937dbc0bbe840d064350a56ae58:0", + "last_update": 1710785638, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "441", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710785638, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "250", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1707848075, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "node2_pub": "025f1456582e70c4c06b61d5c8ed3ce229e6d0db538be337a2dc6d163b0ebc05a5", + "channel_id": "908718872592187392", + "chan_point": "fbb0146d1c27898e7aed5d29383c29c6e4187219cd2475bcf320bd3962beaf86:0", + "last_update": 1710820457, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710820457, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "819", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710807238, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "02404d14a1a5ffdf7f02a04b1aea3941a9ff3184b9ef2fd2c78af862cde9572188", + "node2_pub": "03c41fde73d7853d07c0b0baf110e91c5f54f768cc94d58ddc0d3e947fa7d27e29", + "channel_id": "908794739123027969", + "chan_point": "dddb9425d70ccac822a0ba70caea446c35fd5b4aa688f0cfa811fade74aeb4d5:1", + "last_update": 1710833461, + "capacity": "2750000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "2695275000", + "last_update": 1710833461, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "250", + "disabled": false, + "max_htlc_msat": "2722500000", + "last_update": 1708774500, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "908833222024953856", + "chan_point": "e0e3646f1a14bd290358cb497a6adad15c9514e9f81e06db1669f897106b2343:0", + "last_update": 1710855838, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "24", + "fee_rate_milli_msat": "2361", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710762647, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "249", + "disabled": false, + "max_htlc_msat": "10000000000", + "last_update": 1710855838, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "037ff12b6a4e4bcb4b944b6d20af08cdff61b3461c1dff0d00a88697414d891bc7", + "channel_id": "909009143810555904", + "chan_point": "47c4d4b5091f746fffeabe3c59b5e80c4117a2f55adc24599dcbc76037baae6d:0", + "last_update": 1710815648, + "capacity": "5263278", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "100000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "5210646000", + "last_update": 1710798106, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "5210646000", + "last_update": 1710815648, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "909127891028279296", + "chan_point": "d8f25f010648f84f5121afb53f5b1a4b23b7bc7f4aa7235ac02f1e700e34ee80:0", + "last_update": 1710796438, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "882", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710796438, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "150", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710777007, + "custom_records": {} + }, + "custom_records": {} }, { - "channel_id": "892895800825741312", - "chan_point": "997c879c28e44b11493fffc1e3bdd41bfab8ebcb815a3b8b0c6d0f7baeabe86b:0", - "last_update": 1710607091, - "capacity": "50000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "channel_id": "909179568003022849", + "chan_point": "baa137019225e3f3722c0d99f881f893ec4458b928b10a5ab1d82f77a0c34c43:1", + "last_update": 1710828747, + "capacity": "50000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", - "disabled": true, - "max_htlc_msat": "49500000", - "last_update": 1710607091, + "fee_base_msat": "0", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710828747, "custom_records": {} }, "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710821035, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "909311509549088769", + "chan_point": "b922eb33b01690ab70e096663b535aebbae04dd8820bd12d01e67181bb649a12:1", + "last_update": 1710856854, + "capacity": "10000000", + "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", - "disabled": true, - "max_htlc_msat": "49500000", - "last_update": 1705276253, + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710841428, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03796678b7111abef10f3ff85b88f81f9cfe81cac7e3628a11af1679ed912757d5", - "node2_pub": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000" + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710856854, + "custom_records": {} + }, + "custom_records": {} }, { - "channel_id": "892768257448935424", - "chan_point": "7b2351fe1f9dc287e376f481ae3e52cb39ad417cb8387087b91054e300ddc486:0", - "last_update": 1710639306, - "capacity": "2000000", + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "channel_id": "909522615680565249", + "chan_point": "c887a5c2d22b13b96575a5db83f8aacda9e539eb9ec73ad83752c1669c53214f:1", + "last_update": 1710843032, + "capacity": "10000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "1310", "disabled": false, - "max_htlc_msat": "2000000000", - "last_update": 1710607091, + "max_htlc_msat": "9900000000", + "last_update": 1710843032, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 34, + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710821035, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "909625969739366402", + "chan_point": "15e7e26c970899bab80128a61493ef8410e9fd1a15fa4ec9c7830353c9b209f7:2", + "last_update": 1710804119, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1798", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710804119, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, "min_htlc": "1", - "fee_base_msat": "5805", - "fee_rate_milli_msat": "6", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710639306, + "max_htlc_msat": "20000000000", + "last_update": 1710414556, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "023bb9937dea9707583e45aa6708af0c5d16d9ca3970f67ab76606f43b7457309d", - "node2_pub": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000" + "custom_records": {} }, { - "channel_id": "915246673262739457", - "chan_point": "4c2e260ea115e813a1255e12a1597e2577216d37d770c3e2328170fc28ffb153:1", - "last_update": 1710772374, - "capacity": "100000", + "node1_pub": "02b91642f44270c805e275ae59289f35d464de20cd3a546e3252710db6bab7c965", + "node2_pub": "02b4f019af3b3fa23681c4cfd81f3031e100d2a2d395f0c9e7924de0a6f9f1079f", + "channel_id": "909922837832728577", + "chan_point": "46cc2b8b35c5d42c2175aa3337540f71a861d70ef04e65db2eaab2c8ed208048:1", + "last_update": 1710841865, + "capacity": "250000", "node1_policy": { "time_lock_delta": 40, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710771876, + "max_htlc_msat": "247500000", + "last_update": 1710841865, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", + "fee_base_msat": "0", "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710772374, + "max_htlc_msat": "247500000", + "last_update": 1710792632, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02e45ec6486ad8d2b2dcfe3e994f89035f7f1975e800bfe56f030d910647601199", - "node2_pub": "0209a06852f1257d70e5cea44a0919871fa20859eb33a62445f774e9fe96247a75" + "custom_records": {} }, { - "channel_id": "888979340321554436", - "chan_point": "0055cf02d8bbc7125784e8fee50605969c6cfe76fa24cea14e9ae66a6be4a7c8:4", - "last_update": 1710793392, - "capacity": "2000000", + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "03e691f81f08c56fa876cc4ef5c9e8b727bd682cf35605be25d48607a802526053", + "channel_id": "910070172455731202", + "chan_point": "ef0c127a0cee70011cc961b6a9d4df565d02eec3b0f89108cf046fa0a3a33500:2", + "last_update": 1710791064, + "capacity": "1000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "100", - "fee_rate_milli_msat": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "58", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710790557, + "max_htlc_msat": "990000000", + "last_update": 1710787639, "custom_records": {} }, "node2_policy": { + "time_lock_delta": 42, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710791064, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "037ff12b6a4e4bcb4b944b6d20af08cdff61b3461c1dff0d00a88697414d891bc7", + "node2_pub": "030a425f5c69a29db30f6740d4e7df8f5612ef9955078ef4497490015464733dc8", + "channel_id": "910303268974821377", + "chan_point": "952333bd48adb3f320bc3f42b5be6f020a5d0d6a5be272f0b69eccdf302f3a5b:1", + "last_update": 1710817661, + "capacity": "3000000", + "node1_policy": { "time_lock_delta": 40, + "min_htlc": "100", + "fee_base_msat": "0", + "fee_rate_milli_msat": "5", + "disabled": false, + "max_htlc_msat": "2940300000", + "last_update": 1710817661, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "100000", + "fee_base_msat": "10000", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710814306, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "03c2abfa93eacec04721c019644584424aab2ba4dff3ac9bdab4e9c97007491dda", + "channel_id": "910319761537892352", + "chan_point": "e357d028134934f07317e7160ac73a6521862f2d3f2f042e828fdf53eb204f72:0", + "last_update": 1710840163, + "capacity": "1990000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "222", + "disabled": false, + "max_htlc_msat": "1900000000", + "last_update": 1710778013, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 20, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "1970100000", + "last_update": 1710840163, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "910347249392549889", + "chan_point": "77c7e11f89d02751adc6617367aa2c5c54f40df07608a3b8fcbc61c12b059657:1", + "last_update": 1710856631, + "capacity": "5000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710827025, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710793392, + "max_htlc_msat": "4950000000", + "last_update": 1710856631, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8" + "custom_records": {} }, { - "channel_id": "886619788490833928", - "chan_point": "fe88ddff03886631613cfb2bf0f80849f8074bb2f4269c05de8abcc110741e65:8", - "last_update": 1710791589, - "capacity": "2000000", + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "910361543043186689", + "chan_point": "35989c59c96ed7067682113d339c364809e1064e8f53f22eea951a38a686b151:1", + "last_update": 1710854701, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9405000000", + "last_update": 1710854701, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710841102, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "channel_id": "910426414165262339", + "chan_point": "b1a26a7ab60d8e0711ecbb9e6db125131e434c6e110566b1456c2f80681b9dbe:3", + "last_update": 1710798607, + "capacity": "10000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1000", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710789935, + "max_htlc_msat": "9900000000", + "last_update": 1710796731, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710798607, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "channel_id": "910426414165262342", + "chan_point": "b1a26a7ab60d8e0711ecbb9e6db125131e434c6e110566b1456c2f80681b9dbe:6", + "last_update": 1710840007, + "capacity": "21000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710777972, "custom_records": {} }, "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "120", + "disabled": false, + "max_htlc_msat": "20790000000", + "last_update": 1710840007, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "910493484353191941", + "chan_point": "4485a7608cfc9c53184d7b6d2fe56bf0a77902c89bab595cfbd908390cdc5f91:5", + "last_update": 1710797221, + "capacity": "15000000", + "node1_policy": { "time_lock_delta": 40, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710791589, + "max_htlc_msat": "14850000000", + "last_update": 1710797221, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "400", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710778807, + "custom_records": {} + }, + "custom_records": {} }, { - "channel_id": "914391253219868685", - "chan_point": "acf8a90a8ba334f408b13a1e581538c6fcc3d2398293a59b5fcad77f26769ca2:13", - "last_update": 1710813189, - "capacity": "3000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "910568251362770945", + "chan_point": "6913d18381b743c5a4a3c11562329f790c3e256d9b4b5ccea7ec230d2571db7a:1", + "last_update": 1710804302, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "660", + "disabled": false, + "max_htlc_msat": "7425000000", + "last_update": 1710804302, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710755882, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "026e44acf41fcfa19d092a297a7e8452f6ac5eee677ed0f7f2e5d4c7c632467224", + "channel_id": "910568251362770949", + "chan_point": "6913d18381b743c5a4a3c11562329f790c3e256d9b4b5ccea7ec230d2571db7a:5", + "last_update": 1710849835, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "50000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "305", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710848402, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710849835, + "custom_records": {} + }, + "custom_records": {} + }, + { + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "0260fab633066ed7b1d9b9b8a0fac87e1579d1709e874d28a0d171a1f5c43bb877", + "channel_id": "910568251362770950", + "chan_point": "6913d18381b743c5a4a3c11562329f790c3e256d9b4b5ccea7ec230d2571db7a:6", + "last_update": 1710849421, + "capacity": "100000000", "node1_policy": { - "time_lock_delta": 200, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "2000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", "disabled": false, - "max_htlc_msat": "2970000000", - "last_update": 1710789782, + "max_htlc_msat": "99000000000", + "last_update": 1710849421, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "2970000000", - "last_update": 1710813189, + "max_htlc_msat": "99000000000", + "last_update": 1710804835, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + "custom_records": {} }, { - "channel_id": "886786914241085444", - "chan_point": "c34a46e8d7fd82170c9fc87cfcbc848c477a40799d638512bb60054aafd69cf3:4", - "last_update": 1710830106, - "capacity": "2000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "910568251362770955", + "chan_point": "6913d18381b743c5a4a3c11562329f790c3e256d9b4b5ccea7ec230d2571db7a:11", + "last_update": 1710822835, + "capacity": "100000000", "node1_policy": { "time_lock_delta": 80, - "min_htlc": "1000", + "min_htlc": "10000", "fee_base_msat": "0", - "fee_rate_milli_msat": "159", + "fee_rate_milli_msat": "5000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710828768, + "max_htlc_msat": "20000000000", + "last_update": 1710822835, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 144, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_rate_milli_msat": "499", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710830106, + "max_htlc_msat": "20000000000", + "last_update": 1710447911, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + "custom_records": {} }, { - "channel_id": "890068956432236548", - "chan_point": "ffbc1367d23f03fc1b3551faf2832e16668ee135fc780e22e46b72fb025c05a1:4", - "last_update": 1710795190, - "capacity": "2000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "910586942962008065", + "chan_point": "6f768219e7e0d4e027737cc74f115eefb63a54a50c44917d05c06beb69026f05:1", + "last_update": 1710803284, + "capacity": "100000000", "node1_policy": { "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710790263, + "max_htlc_msat": "99000000000", + "last_update": 1710803284, "custom_records": {} }, "node2_policy": { @@ -4235,1129 +21999,1100 @@ "fee_base_msat": "1000", "fee_rate_milli_msat": "10", "disabled": false, - "max_htlc_msat": "1980000000", + "max_htlc_msat": "99000000000", "last_update": 1710795190, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d" + "custom_records": {} }, { - "channel_id": "889913925275484176", - "chan_point": "48cd67b0995fb7e49876f4e18fba74bbde68bf3c713b6cc5ceb93c4f3272f4ee:16", - "last_update": 1710829389, - "capacity": "2000000", + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "910646316663767041", + "chan_point": "cc8fb78edccff1ce73a63a78be2a32418855f7c21c139c8173d4ba904ea20bca:1", + "last_update": 1710856131, + "capacity": "20000000", "node1_policy": { "time_lock_delta": 80, - "min_htlc": "10000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "90", + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710789499, + "max_htlc_msat": "19800000000", + "last_update": 1710836025, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710829389, + "max_htlc_msat": "19800000000", + "last_update": 1710856131, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0" + "custom_records": {} }, { - "channel_id": "888987036985851905", - "chan_point": "094ff6c4b2ee8473bf7839d6d48c389576209ef4c581abb7abcab755bd20a4b8:1", - "last_update": 1710791182, - "capacity": "2000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03c8dfbf829eaeb0b6dab099d87fdf7f8faceb0c1b935cd243e8c1fb5af71361cf", + "channel_id": "910652913616551937", + "chan_point": "e435235eea52cf80e19571f372d6d7040a21984763af9662c4d6ba566b742526:1", + "last_update": 1710852124, + "capacity": "15000000", "node1_policy": { "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710791182, + "max_htlc_msat": "14850000000", + "last_update": 1710852124, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "3000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710775389, + "max_htlc_msat": "14850000000", + "last_update": 1710783313, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + "custom_records": {} }, { - "channel_id": "886894666356949005", - "chan_point": "638f26ea27dfba927dfb3897f9fe9489d604f12b8b7721eb4f76fe09896f9f01:13", - "last_update": 1710841936, - "capacity": "2000000", + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "channel_id": "910669406377869313", + "chan_point": "6a4c18260cc3e7e8f9beecdac99c18e1e0723de35f3cdd7610f4f3a059f8621d:1", + "last_update": 1710854378, + "capacity": "4900000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 144, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "499", + "fee_rate_milli_msat": "210", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710841936, + "max_htlc_msat": "4851000000", + "last_update": 1710854378, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 222, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_rate_milli_msat": "2000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710793390, + "max_htlc_msat": "4851000000", + "last_update": 1710772753, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3" + "custom_records": {} }, { - "channel_id": "889855651147612169", - "chan_point": "7ce4049dacc96cbed13afd0664d05d6d4f68ec7b6d1bd6181fe03736af188fea:9", - "last_update": 1710813189, - "capacity": "2000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03a21a8756ffc55dd1d83a02b0cc2e163f8d041e3f56b309232e5389c66c00364c", + "channel_id": "910686998444048388", + "chan_point": "c46465632d61e9da42138f4d2a01c0b00aa9e34e1b42e9a717272f6c36c4bb7a:4", + "last_update": 1710851760, + "capacity": "15000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1600", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710813189, + "max_htlc_msat": "14850000000", + "last_update": 1710851635, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 222, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "2000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710790753, + "max_htlc_msat": "14850000000", + "last_update": 1710851760, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + "custom_records": {} }, { - "channel_id": "890068956432236557", - "chan_point": "ffbc1367d23f03fc1b3551faf2832e16668ee135fc780e22e46b72fb025c05a1:13", - "last_update": 1710793392, - "capacity": "2000000", + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "910688097982808065", + "chan_point": "bb516ed4c15ffb282f61bf2fdaae9e388c369b9e87bcc5031193a627480bfffb:1", + "last_update": 1710855749, + "capacity": "30000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "4000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710793392, + "max_htlc_msat": "13500000000", + "last_update": 1710855749, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", + "time_lock_delta": 144, + "min_htlc": "1", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "999", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710789318, + "max_htlc_msat": "13500000000", + "last_update": 1710688399, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", - "node2_pub": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96" + "custom_records": {} }, { - "channel_id": "918488033557348353", - "chan_point": "3c57def65d9db037ef262b94448e0b19413365d50874dadf6c8706704ef2a6a0:1", - "last_update": 1710855657, - "capacity": "16770000", + "node1_pub": "03ec512342aeee370b53d9fd12dbd5283dcd670d248018b3a1cf537313e76e6a2d", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "910818939924774912", + "chan_point": "02e6c31e514e1b09fff9da0d3de36b3060d4c2df87a418295baaa2d8d9c83580:0", + "last_update": 1710828417, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 72, + "time_lock_delta": 80, "min_htlc": "1", - "fee_base_msat": "2147483647", - "fee_rate_milli_msat": "2147483647", + "fee_base_msat": "811", + "fee_rate_milli_msat": "237", "disabled": false, - "max_htlc_msat": "15093000000", - "last_update": 1710855657, + "max_htlc_msat": "9900000000", + "last_update": 1710828417, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "500", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1985", "disabled": false, - "max_htlc_msat": "1677000000", - "last_update": 1710855493, + "max_htlc_msat": "10000000000", + "last_update": 1710775531, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", - "node2_pub": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17" + "custom_records": {} }, { - "channel_id": "913633689575358465", - "chan_point": "f4ba21d0d25d0d064fab44160da431b099ffa8f7fe5a7d1289a4ed3d2ce3c60b:1", - "last_update": 1710822543, - "capacity": "25000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "channel_id": "910991563189387264", + "chan_point": "93faf32b05528bae4ceaf0e212157ad2c3ff62985df47bbcc5277c3fd3f87022:0", + "last_update": 1710821035, + "capacity": "100000000", "node1_policy": { "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1000", + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1750", "disabled": false, - "max_htlc_msat": "24750000000", - "last_update": 1710789935, + "max_htlc_msat": "99000000000", + "last_update": 1710821035, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "500", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "24750000000", - "last_update": 1710822543, + "max_htlc_msat": "99000000000", + "last_update": 1710782734, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", - "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + "custom_records": {} }, { - "channel_id": "914491308634472449", - "chan_point": "a0e58782bcff1c828a74744932f8ab32bafc27b275001372c2e62b5955387602:1", - "last_update": 1710790143, - "capacity": "100000000", + "node1_pub": "032b09fb582a7a0718a606ee075598b0ed2e04da376382db03d76aded6e56216c6", + "node2_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "channel_id": "911436865352499201", + "chan_point": "29d778a7e2c22d332aa85870a99e3c5c64d026387e702892dce77bbdd59ad161:1", + "last_update": 1710839832, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 50, "min_htlc": "1000", "fee_base_msat": "0", "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "99000000000", - "last_update": 1710790037, + "max_htlc_msat": "9900000000", + "last_update": 1710839832, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 120, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "0", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", "disabled": false, - "max_htlc_msat": "99000000000", - "last_update": 1710790143, + "max_htlc_msat": "280000000", + "last_update": 1710803683, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", - "node2_pub": "0294774ee02a9faa5a5870061f7f4833686184ad14a0b163c49442516c9edac1db" + "custom_records": {} }, { - "channel_id": "918488033557217280", - "chan_point": "9fd21e0a421924545e30b891812695791696c676367094ffe2467460eff56c36:0", - "last_update": 1710855500, - "capacity": "16770000", + "node1_pub": "03213c5175050fc1dfe0b4560c0cd5c6dd6bf4b25c2f11df0285384d0402e0d397", + "node2_pub": "021a7a31f03a9b49807eb18ef03046e264871a1d03cd4cb80d37265499d1b726b9", + "channel_id": "911491840958857217", + "chan_point": "ac7684a397733c23ecc00d1403224ce1d764e9b98dd8879d2c43a965e3c8cba6:1", + "last_update": 1710827955, + "capacity": "5000000", "node1_policy": { - "time_lock_delta": 72, - "min_htlc": "1", - "fee_base_msat": "2147483647", - "fee_rate_milli_msat": "2147483647", - "disabled": true, - "max_htlc_msat": "15093000000", - "last_update": 1710855462, + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "261", + "disabled": false, + "max_htlc_msat": "4950000000", + "last_update": 1710825485, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "500", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "5000", "disabled": false, - "max_htlc_msat": "1677000000", - "last_update": 1710855500, + "max_htlc_msat": "4950000000", + "last_update": 1710827955, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", - "node2_pub": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20" + "custom_records": {} }, { - "channel_id": "917417109119041536", - "chan_point": "9af4014c8a9dafc53ef2913a4bde5e2111cf10205c1905ae585e5511de108bcc:0", - "last_update": 1710854361, - "capacity": "99995000", + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "channel_id": "911605090606972929", + "chan_point": "301210f850411a867cd87f3d46df8fe42e7ff585a5e9143ade2d7ae51352130f:1", + "last_update": 1710845714, + "capacity": "300000000", "node1_policy": { - "time_lock_delta": 144, - "min_htlc": "1", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "999", + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2000", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710854361, + "max_htlc_msat": "297000000000", + "last_update": 1710842454, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "750", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710822543, + "max_htlc_msat": "297000000000", + "last_update": 1710845714, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "914489109751398401", - "chan_point": "c51bb911ae676ddcbd7207a596dbea51151ba678399287ab8221f27cf93fdbf2:1", - "last_update": 1710790753, - "capacity": "25000000", + "node1_pub": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "912080079757312001", + "chan_point": "0b9d0effb6cbb96fabf0e26c93743a8db7a67cdcb3287e2f75f410a0dc336481:1", + "last_update": 1710835555, + "capacity": "16777215", "node1_policy": { - "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "500", + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "5292", + "fee_rate_milli_msat": "2646", "disabled": false, - "max_htlc_msat": "24750000000", - "last_update": 1710777543, + "max_htlc_msat": "7549746000", + "last_update": 1710835555, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 222, - "min_htlc": "1000", + "time_lock_delta": 144, + "min_htlc": "0", "fee_base_msat": "1000", - "fee_rate_milli_msat": "2000", + "fee_rate_milli_msat": "499", "disabled": false, - "max_htlc_msat": "24750000000", - "last_update": 1710790753, + "max_htlc_msat": "7549746000", + "last_update": 1710000163, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", - "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + "custom_records": {} }, { - "channel_id": "850763614782029825", - "chan_point": "595e98da49698fdb9d9e25fbe9279c7f15897193bb13da9faef8ef1213a13fa5:1", - "last_update": 1710609731, - "capacity": "500000", + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "912271394755641344", + "chan_point": "f4f6591ee6e71a122c765f1d676943657d66a816ee2f22c404f739e8b9c58a19:0", + "last_update": 1710837838, + "capacity": "50000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 100, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "50", + "fee_base_msat": "500", + "fee_rate_milli_msat": "567", "disabled": false, - "max_htlc_msat": "495000000", - "last_update": 1707058294, + "max_htlc_msat": "49500000000", + "last_update": 1710837838, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "300", - "disabled": true, - "max_htlc_msat": "495000000", - "last_update": 1710609731, + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710786654, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "027d5369f807773ed75dc87dd12c585210b72827263f4a1009f052e67c12ac92f5", - "node2_pub": "02aeb397b7212dc1de5252684f9a1f1957d8bcc9864193b538ce78f9558685d666" + "custom_records": {} }, { - "channel_id": "912038298208501760", - "chan_point": "42df48fd0764c7777e7e0f290e9538f3082222648941e6078c348f6f02a4457d:0", - "last_update": 1710855461, - "capacity": "6710669", + "node1_pub": "03bc9337c7a28bb784d67742ebedd30a93bacdf7e4ca16436ef3798000242b2251", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "912555068775071745", + "chan_point": "b70554c80fe432384f58868977023a98c56c51f060ebac51774a6e8c3e74a3ed:1", + "last_update": 1710842648, + "capacity": "3045714", "node1_policy": { - "time_lock_delta": 140, - "min_htlc": "100", - "fee_base_msat": "2000", - "fee_rate_milli_msat": "2500", + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", "disabled": false, - "max_htlc_msat": "6643563000", - "last_update": 1710855461, + "max_htlc_msat": "100000000", + "last_update": 1710758012, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "500", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "800", "disabled": false, - "max_htlc_msat": "6643563000", - "last_update": 1710846063, + "max_htlc_msat": "1370571000", + "last_update": 1710842648, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d", - "node2_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149" + "custom_records": {} }, { - "channel_id": "891582983965769729", - "chan_point": "2dd71734bb100d72ae15439e6e707f6ca451040f21dc8f79df75d8fac835caf4:1", - "last_update": 1710855891, - "capacity": "10000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "912558367295537154", + "chan_point": "e0576d85be3e8e5a2539a145e7e280b8446cc1542806e790d369a2828faf43bd:2", + "last_update": 1710855529, + "capacity": "12500000", "node1_policy": { - "time_lock_delta": 140, - "min_htlc": "100", - "fee_base_msat": "2000", - "fee_rate_milli_msat": "2500", + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "9900000000", - "last_update": 1710855891, + "max_htlc_msat": "12375000000", + "last_update": 1710855529, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, - "min_htlc": "1000", + "min_htlc": "10000", "fee_base_msat": "0", - "fee_rate_milli_msat": "500", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "9900000000", - "last_update": 1710847863, + "max_htlc_msat": "12375000000", + "last_update": 1710839035, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d", - "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + "custom_records": {} }, { - "channel_id": "854880186313605121", - "chan_point": "78dc9a67189d0d44d3678cb35bb8b297fde81743c3ebdfaa1651fe4ad731542e:1", - "last_update": 1710786746, - "capacity": "1000000", + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "912821150514741248", + "chan_point": "6b228c36e358af7c041ac830b3a8ec3851c1b5d31b32cc577d5f941ee20513a1:0", + "last_update": 1710849107, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", "disabled": false, - "max_htlc_msat": "990000000", - "last_update": 1710770149, + "max_htlc_msat": "9900000000", + "last_update": 1710838854, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "521", "disabled": false, - "max_htlc_msat": "990000000", - "last_update": 1710786746, + "max_htlc_msat": "9900000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", - "node2_pub": "02abb5e57ff442770d9dc1d1ab66f45b015ac7aa033e4407051d060a471b71b08b" + "custom_records": {} }, { - "channel_id": "781908898078588929", - "chan_point": "2d4c7735462b434dc79bd300e9d788cb78af0643c668531dab090c5d539fb3d1:1", - "last_update": 1710844710, - "capacity": "1500000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "912825548574162945", + "chan_point": "1b9643cced03480ce9f4bfe7a4b3472392809f94cc5699e0924b8e44b0506e02:1", + "last_update": 1710849107, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "1500000000", - "last_update": 1710844710, + "max_htlc_msat": "9900000000", + "last_update": 1710783235, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, - "min_htlc": "1", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "319", "disabled": false, - "max_htlc_msat": "400000000", - "last_update": 1710684918, + "max_htlc_msat": "9900000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "793454869742813185", - "chan_point": "5327c98b3ce6a42c8baeebdc740bf24fbf0bedf5aa1f56eeebcc2b58fab5d54f:1", - "last_update": 1710791299, - "capacity": "2000000", + "node1_pub": "026f46207fd290a33cbd86e29b3ad0a47cdd44ab9aa5267cde66483e10aa9d3180", + "node2_pub": "024efd67cb4e01cdcd79e86f561cb5e66faea6d3761b5f33daca6afce02a80a7df", + "channel_id": "912961888014761984", + "chan_point": "29129a9d5f8d62561c3666a06c00ad46eb6ac32a1c8393795dce524fd9f55eca:0", + "last_update": 1710845085, + "capacity": "5000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 144, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "90", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710787110, + "max_htlc_msat": "4950000000", + "last_update": 1710845085, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "10000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "90", + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710791299, + "max_htlc_msat": "4950000000", + "last_update": 1710796690, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", - "node2_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0" + "custom_records": {} }, { - "channel_id": "835777271311630337", - "chan_point": "26bedad6cbd11dd2054557a51631971287141b0b487ef09e1482601e8f24f28f:1", - "last_update": 1710785354, - "capacity": "1620000", + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "913098227473121280", + "chan_point": "1ffe54a97cb8d8cf0b80764f5b6d6fcd38b409731f58c2f3ec3ac2df0dc2d408:0", + "last_update": 1710809725, + "capacity": "250000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "1603800000", - "last_update": 1710778110, + "max_htlc_msat": "20000000000", + "last_update": 1710809725, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 222, - "min_htlc": "1000", + "time_lock_delta": 144, + "min_htlc": "1", "fee_base_msat": "1000", - "fee_rate_milli_msat": "50", + "fee_rate_milli_msat": "999", "disabled": false, - "max_htlc_msat": "1603800000", - "last_update": 1710785354, + "max_htlc_msat": "10000000", + "last_update": 1710758532, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", - "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + "custom_records": {} }, { - "channel_id": "817806853236391936", - "chan_point": "2d7da2726a4724f8a350315840bd7e45b383c4e909e749b235d62339880b67fb:0", - "last_update": 1710791182, - "capacity": "90000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "913132312309661698", + "chan_point": "f323335ab1c8fbf360a4cb38b39463d7f6b484b8ad8fd00db8bbfcdfbb9786c6:2", + "last_update": 1710807743, + "capacity": "25000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "4610", "disabled": false, - "max_htlc_msat": "89100000", - "last_update": 1710509231, + "max_htlc_msat": "24750000000", + "last_update": 1710807743, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", + "time_lock_delta": 80, + "min_htlc": "10000", "fee_base_msat": "0", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "600", "disabled": false, - "max_htlc_msat": "89100000", - "last_update": 1710791182, + "max_htlc_msat": "25000000000", + "last_update": 1710776035, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02bf145fe009fef1230f3f435b5e448db3470a1fdbff95d41cefa71b7d6f14fcda", - "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + "custom_records": {} }, { - "channel_id": "885613735298072577", - "chan_point": "aae3c5dac5bc5a9b5813bb1f37592ca1aa0747d3b8089facdbf584d368ba94cb:1", - "last_update": 1710846929, - "capacity": "24639", + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "913211477159444481", + "chan_point": "1a70c078693acdd62603f9ac2adab67507a511851da6f44a9492f7ec3d5aae5a:1", + "last_update": 1710814907, + "capacity": "10000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "50", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "24393000", - "last_update": 1710710327, + "max_htlc_msat": "9900000000", + "last_update": 1710814363, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "24393000", - "last_update": 1710846929, + "max_htlc_msat": "9900000000", + "last_update": 1710814907, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296", - "node2_pub": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac" + "custom_records": {} }, { - "channel_id": "817627632791715840", - "chan_point": "349640c4fbb05af12ebc1bc904758dae19e90b3aa3ad2de840ad3b7c84889e56:0", - "last_update": 1710803782, - "capacity": "80000", + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "913211477159444483", + "chan_point": "1a70c078693acdd62603f9ac2adab67507a511851da6f44a9492f7ec3d5aae5a:3", + "last_update": 1710849107, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", + "time_lock_delta": 144, + "min_htlc": "1", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "499", "disabled": false, - "max_htlc_msat": "79200000", - "last_update": 1710775588, + "max_htlc_msat": "4500000000", + "last_update": 1710697901, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", + "time_lock_delta": 100, + "min_htlc": "100000", "fee_base_msat": "0", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "908", "disabled": false, - "max_htlc_msat": "79200000", - "last_update": 1710803782, + "max_htlc_msat": "4500000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296", - "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + "custom_records": {} }, { - "channel_id": "868515230059724800", - "chan_point": "5c1b233a9778af0356a5e02dd8d758ebb8b85ddbcf8252d793217d1a28dedd67:0", - "last_update": 1710635188, - "capacity": "80000", + "node1_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "913218074207059969", + "chan_point": "b0a1f85a4ef3d92896f9e03a0ddf92d97fae54c76ecbcb3377f1b78ea7eac0eb:1", + "last_update": 1710849107, + "capacity": "15000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", - "disabled": true, - "max_htlc_msat": "79200000", - "last_update": 1710635188, + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "14850000000", + "last_update": 1710784535, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "72", "disabled": false, - "max_htlc_msat": "79200000", - "last_update": 1707066297, + "max_htlc_msat": "14850000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296", - "node2_pub": "02f5be5f3d54b66bf531b96f06327f59cd139f6e2c301cc0131a3f632025c2704c" + "custom_records": {} }, { - "channel_id": "909468739601629185", - "chan_point": "ca9c5d9014ff6701875ec69d4db910fb7592f39ed5ad44eac23ff1f1031267f2:1", - "last_update": 1710783381, - "capacity": "500000", + "node1_pub": "034ea80f8b148c750463546bd999bf7321a0e6dfc60aaf84bd0400a2e8d376c0d5", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "913298338556280834", + "chan_point": "122820a930e49889b21046cc5ec76f438eef99a6109b96f5cab36b3ac3015150:2", + "last_update": 1710849107, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "167", - "disabled": true, - "max_htlc_msat": "495000000", - "last_update": 1710783379, + "fee_rate_milli_msat": "800", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710784681, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "100", - "fee_rate_milli_msat": "100", - "disabled": true, - "max_htlc_msat": "495000000", - "last_update": 1710783381, + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1299", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02c87bce43398e73f65df561b4e776306fc1e74657d67131d76866ba617a0e63c2", - "node2_pub": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880" + "custom_records": {} }, { - "channel_id": "913193885013966849", - "chan_point": "087e6845eefd047bb0dd3ee591a2dbea28b504b8a07fd733f5eb51bf89b0ca24:1", - "last_update": 1710834571, - "capacity": "500000", + "node1_pub": "033d8656219478701227199cbd6f670335c8d408a92ae88b962c49d4dc0e83e025", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "913298338556280835", + "chan_point": "122820a930e49889b21046cc5ec76f438eef99a6109b96f5cab36b3ac3015150:3", + "last_update": 1710849107, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "100", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "495000000", - "last_update": 1710779061, + "max_htlc_msat": "9900000000", + "last_update": 1710802631, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 80, - "min_htlc": "1000", + "time_lock_delta": 100, + "min_htlc": "100000", "fee_base_msat": "0", - "fee_rate_milli_msat": "30", + "fee_rate_milli_msat": "1280", "disabled": false, - "max_htlc_msat": "495000000", - "last_update": 1710834571, + "max_htlc_msat": "9900000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02c87bce43398e73f65df561b4e776306fc1e74657d67131d76866ba617a0e63c2", - "node2_pub": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + "custom_records": {} }, { - "channel_id": "769006129141776384", - "chan_point": "e5ebf62a8e888892b505156f95c8c6c7cb60ecf9801ec9f6650d3f303e53cba9:0", - "last_update": 1710704923, - "capacity": "250000", + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "channel_id": "913301637187371009", + "chan_point": "2a5dc9d835f9227920037110c442f45ff5c59d3ade08d4ccc8c6ffdc58ab49ff:1", + "last_update": 1710845485, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "247500000", - "last_update": 1710531493, + "max_htlc_msat": "9900000000", + "last_update": 1710845485, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "247500000", - "last_update": 1710704923, + "max_htlc_msat": "9900000000", + "last_update": 1710653670, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02b196899ff33d892562c557a37f56a3702f14eba8aee8585625ed8cf4217f2ab3", - "node2_pub": "021227d4be948ab84613d5dbd47b6e148cfb811432d27eb92e80d28382b1b27d27" + "custom_records": {} }, { - "channel_id": "770339836665790464", - "chan_point": "fd89d822c95341eb4948000720b9747ce4b517e3ec6726db5774bdda4f25164c:0", - "last_update": 1710773323, - "capacity": "2000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "channel_id": "913301637187371010", + "chan_point": "2a5dc9d835f9227920037110c442f45ff5c59d3ade08d4ccc8c6ffdc58ab49ff:2", + "last_update": 1710852963, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710771935, + "max_htlc_msat": "9900000000", + "last_update": 1710845485, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710773323, + "max_htlc_msat": "9900000000", + "last_update": 1710852963, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02b196899ff33d892562c557a37f56a3702f14eba8aee8585625ed8cf4217f2ab3", - "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + "custom_records": {} }, { - "channel_id": "860278788451991553", - "chan_point": "34f76ac4003f888ceeeeb9f71b3912ca731bebf78c3a808d3abfb4abd5beb96a:1", - "last_update": 1710851757, - "capacity": "100000", + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "913374204975120385", + "chan_point": "27a36c1b36c1aba4d880060e9261df46a9ec028c6d1117d79692c72b17671982:1", + "last_update": 1710801640, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 120, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "600", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710851757, + "max_htlc_msat": "30000000", + "last_update": 1710801640, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710817465, + "max_htlc_msat": "9900000000", + "last_update": 1710796907, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96", - "node2_pub": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8" + "custom_records": {} }, { - "channel_id": "865665295861612544", - "chan_point": "95a81b50d4b7055f0af96915ff3226e0d8b97d6a1e3d6975e02a295301468c8f:0", - "last_update": 1710594913, - "capacity": "100000", + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "913442374560710658", + "chan_point": "b965586ebc9f708b5d063a0398715dea514745e1cdbbf2a733ca6181668bfe0c:2", + "last_update": 1710849107, + "capacity": "20000000", "node1_policy": { "time_lock_delta": 40, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "10", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710125353, + "max_htlc_msat": "19800000000", + "last_update": 1710795190, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "310", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710594913, + "max_htlc_msat": "19800000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96", - "node2_pub": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43" + "custom_records": {} }, { - "channel_id": "850260038455197696", - "chan_point": "81b147059ab5ee28ccb9e1af5158254e6a0ceb3170ca1ebd2c062bad44ddf479:0", - "last_update": 1710798303, - "capacity": "5000223", + "node1_pub": "03271338633d2d37b285dae4df40b413d8c6c791fbee7797bc5dc70812196d7d5c", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "913475359943426049", + "chan_point": "fb61d122c3dc8c644a7d04b0b4f1a8a7c7b01ac155a88daa01454d89b5db4167:1", + "last_update": 1710849107, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "0", - "disabled": false, - "max_htlc_msat": "4950221000", - "last_update": 1710798303, - "custom_records": {} - }, - "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "4950221000", - "last_update": 1710798303, - "custom_records": {} - }, - "custom_records": {}, - "node1_pub": "02aeb397b7212dc1de5252684f9a1f1957d8bcc9864193b538ce78f9558685d666", - "node2_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403" - }, - { - "channel_id": "918392376041340929", - "chan_point": "f494169e156a65a8cfb628bd036a591d659af6aadff5604c89c0faf7fc92d527:1", - "last_update": 1710841167, - "capacity": "16770000", - "node1_policy": { - "time_lock_delta": 140, - "min_htlc": "100", - "fee_base_msat": "2000", - "fee_rate_milli_msat": "2500", - "disabled": false, - "max_htlc_msat": "1677000000", - "last_update": 1710841167, + "max_htlc_msat": "9900000000", + "last_update": 1710784131, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 72, - "min_htlc": "1000", - "fee_base_msat": "2147483647", - "fee_rate_milli_msat": "2147483647", + "time_lock_delta": 100, + "min_htlc": "100000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "213", "disabled": false, - "max_htlc_msat": "15093000000", - "last_update": 1710802200, + "max_htlc_msat": "9900000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20", - "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + "custom_records": {} }, { - "channel_id": "918425361248288768", - "chan_point": "036f4a9be00df25cc8abfcdeeae828e0fbc7d82c92109e907aaf9837501680f7:0", - "last_update": 1710829005, - "capacity": "16770000", + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "913799715935813632", + "chan_point": "5fcb5970932ce7e05a6a70d78eae81f43735e02d4e2a4b3a1950b7155a6a6419:0", + "last_update": 1710850636, + "capacity": "20000000", "node1_policy": { "time_lock_delta": 80, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1000", - "disabled": true, - "max_htlc_msat": "1677000000", - "last_update": 1710829005, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "222", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710850636, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 72, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "2147483647", - "fee_rate_milli_msat": "2147483647", + "fee_base_msat": "0", + "fee_rate_milli_msat": "127", "disabled": false, - "max_htlc_msat": "15093000000", - "last_update": 1710816411, + "max_htlc_msat": "20000000000", + "last_update": 1710838039, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20", - "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + "custom_records": {} }, { - "channel_id": "763640512349143040", - "chan_point": "50ad5a9f0c31ec00e8e5dfc4a3947ea363542be0db57a538c014d1693c00782f:0", - "last_update": 1710398772, - "capacity": "100000", + "node1_pub": "035b1ff29e8db1ba8f2a4f4f95db239b54069cb949b8cde329418e2a83da4f1b30", + "node2_pub": "02d695b01c7a6909e716c863fb39bc5fb7bbdc3824b7fdce53adc593e5be080e73", + "channel_id": "913801914890452992", + "chan_point": "3eca8c0d91d34e5bb33e1c07806d7f7a9c53271586894c91ed5d8d1ad567e17b:0", + "last_update": 1710820876, + "capacity": "20000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "20", + "time_lock_delta": 80, + "min_htlc": "1", + "fee_base_msat": "1", + "fee_rate_milli_msat": "88", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1709879445, + "max_htlc_msat": "19800000000", + "last_update": 1710807743, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 120, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_rate_milli_msat": "600", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710398772, + "max_htlc_msat": "570000000", + "last_update": 1710820876, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0202b5d54467d075893d46b6205067bbab2bcbc632513cd15661ad803d2dcb4ce3", - "node2_pub": "03b9de7a7d7e8a474b859a59e314cf2a143ae9c569ce78c46289b3ad16cb8c03f9" + "custom_records": {} }, { - "channel_id": "908048170593550337", - "chan_point": "0d45ab108774a44b8ed9a0a09c6ae6ac6390447c8c4f31762c9984e268dbf63a:1", - "last_update": 1710856405, - "capacity": "500000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "020d1617e27ac022395352f2b3774969593d3d6ddff6fb117d820a9dda8da45217", + "channel_id": "914147161649184768", + "chan_point": "8f6adbc61fbe2c49b6dfc952596a24d9bef00e74110b4efe952b49d259494099:0", + "last_update": 1710806635, + "capacity": "35000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "100", - "fee_rate_milli_msat": "1", + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "153", "disabled": false, - "max_htlc_msat": "495000000", - "last_update": 1710856405, + "max_htlc_msat": "34650000000", + "last_update": 1710734645, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "100", - "fee_rate_milli_msat": "100", + "time_lock_delta": 80, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2000", "disabled": false, - "max_htlc_msat": "495000000", - "last_update": 1710801772, + "max_htlc_msat": "35000000000", + "last_update": 1710806635, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0202b5d54467d075893d46b6205067bbab2bcbc632513cd15661ad803d2dcb4ce3", - "node2_pub": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880" + "custom_records": {} }, { - "channel_id": "756393631196446720", - "chan_point": "f64829c1e94063373474fc4be07bb1e594b4fd639be17b437af1d84fa1c629c1:0", - "last_update": 1710811915, - "capacity": "1000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "channel_id": "914193341093511170", + "chan_point": "b565410e7cbcd0c7422bbd2274208cf042a1c3feed1e6f9ad3b9bb90e5f7605c:2", + "last_update": 1710804343, + "capacity": "25000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 144, "min_htlc": "1000", - "fee_base_msat": "0", + "fee_base_msat": "1000", "fee_rate_milli_msat": "2000", "disabled": false, - "max_htlc_msat": "990000000", - "last_update": 1662603210, + "max_htlc_msat": "24750000000", + "last_update": 1710804343, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", + "time_lock_delta": 80, + "min_htlc": "10000", "fee_base_msat": "0", - "fee_rate_milli_msat": "750", - "disabled": true, - "max_htlc_msat": "990000000", - "last_update": 1710811915, + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710803035, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403", - "node2_pub": "024a65932bdad1c4ef070289a6dd9f71b315e61d871b38c79339d0cc3d26e100e1" + "custom_records": {} }, { - "channel_id": "771570190275051521", - "chan_point": "6b2d5942bdadd04f26ad4f7ee86cceabd68531f66c7b370d27e02d9222c02bd1:1", - "last_update": 1710819115, - "capacity": "1000000", + "node1_pub": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "914263709754523648", + "chan_point": "91e45024cc8e0677e64f20c0288f179a63c94931d84ac0dbcfcf28010992e2ee:0", + "last_update": 1710843510, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "750", - "disabled": true, - "max_htlc_msat": "990000000", - "last_update": 1710819115, + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4500000000", + "last_update": 1710843510, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "200", - "fee_rate_milli_msat": "200", + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", "disabled": false, - "max_htlc_msat": "990000000", - "last_update": 1648881967, + "max_htlc_msat": "4500000000", + "last_update": 1710325293, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403", - "node2_pub": "03d213f4115a55f0d0ab185434fcb6d3f119fdede11e901fe57f8b91d57ed316e6" + "custom_records": {} }, { - "channel_id": "887983182865104897", - "chan_point": "713685d0215c28e1ea8f56357459afb65fc86fd26075d2cc760589ab82f2f053:1", - "last_update": 1710842515, - "capacity": "50000000", + "node1_pub": "02ba3ad33666de22b4c22f5ff9fac0dc5d18ae9b6ce38c0a06d9e171494c39255a", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "914351670755065860", + "chan_point": "a79f8f42050337c50f62f854153a18dcc6a569c579c41dd8ceda1db7712b5c21:4", + "last_update": 1710829389, + "capacity": "3000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710842515, + "max_htlc_msat": "2970000000", + "last_update": 1710789510, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, - "min_htlc": "1", + "time_lock_delta": 80, + "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710246269, + "max_htlc_msat": "2970000000", + "last_update": 1710829389, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "756389233140760576", - "chan_point": "207b45f0cb6cb068b332bbdade71c936097d97277aafc212fea2548598398291:0", - "last_update": 1710393060, - "capacity": "250000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "914356068737351680", + "chan_point": "a65ab52cda51875300c1fe4b43bb165e67d05cc4819419f2de32b9a0fa9d7e36:0", + "last_update": 1710789238, + "capacity": "100000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 100, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1000", - "disabled": true, - "max_htlc_msat": "247500000", - "last_update": 1710393060, + "fee_base_msat": "0", + "fee_rate_milli_msat": "441", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710789238, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "100", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10000", "disabled": false, - "max_htlc_msat": "247500000", - "last_update": 1662580373, + "max_htlc_msat": "99000000000", + "last_update": 1710783235, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "024a65932bdad1c4ef070289a6dd9f71b315e61d871b38c79339d0cc3d26e100e1", - "node2_pub": "021227d4be948ab84613d5dbd47b6e148cfb811432d27eb92e80d28382b1b27d27" + "custom_records": {} }, { - "channel_id": "918390177017757696", - "chan_point": "d46c48c0d9410ab9597860b8ba10c14ed4c1d86338f80a7f7ff36ecf881269b3:0", - "last_update": 1710806052, - "capacity": "16770000", + "node1_pub": "030057ffea1a1650ce716aab702c9fc29ce24659b89650eb963f2455df0194c997", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "914357168314974208", + "chan_point": "98d2fb70999e71b86815dec7b676d83e4e5f8ffaea2ed3ddc860d0416f915947:0", + "last_update": 1710854589, + "capacity": "3000000", "node1_policy": { - "time_lock_delta": 72, - "min_htlc": "1000", - "fee_base_msat": "2147483647", - "fee_rate_milli_msat": "2147483647", + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "8244", + "fee_rate_milli_msat": "833", "disabled": false, - "max_htlc_msat": "15093000000", - "last_update": 1710804699, + "max_htlc_msat": "2970000000", + "last_update": 1710835407, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 140, - "min_htlc": "100", - "fee_base_msat": "2000", - "fee_rate_milli_msat": "2500", + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "1677000000", - "last_update": 1710806052, + "max_htlc_msat": "3000000000", + "last_update": 1710854589, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17", - "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + "custom_records": {} }, { - "channel_id": "899069558556065792", - "chan_point": "9483b700e1dfa3512defbb27097dc48da750ae6ea07b3baeb3c1a34da4cdec52:0", - "last_update": 1710846357, - "capacity": "40000", + "node1_pub": "02f7467f4de732f3b3cffc8d5e007aecdf6e58878edb6e46a8e80164421c1b90aa", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "914357168314974214", + "chan_point": "98d2fb70999e71b86815dec7b676d83e4e5f8ffaea2ed3ddc860d0416f915947:6", + "last_update": 1710795190, + "capacity": "3000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "100", - "fee_rate_milli_msat": "10", - "disabled": true, - "max_htlc_msat": "39600000", - "last_update": 1710846357, + "fee_base_msat": "1000", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710789197, "custom_records": {} }, "node2_policy": { @@ -5366,541 +23101,541 @@ "fee_base_msat": "1000", "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "39600000", - "last_update": 1700598649, + "max_htlc_msat": "2970000000", + "last_update": 1710795190, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8", - "node2_pub": "0321743efb2d44a9c1ff8194f03728e119bbf6cde88201b9ac8e6e48f96c99d597" + "custom_records": {} }, { - "channel_id": "877726938508689409", - "chan_point": "5175773efb30bcae88dca5c47c6dcf8ba78b3fd8c31397d08db8f7b9e7e85019:1", - "last_update": 1710785354, - "capacity": "84803717", + "node1_pub": "021744d86987a91958461117cd9e7c0e3160f7b86de11f5998018f4b4984a5c330", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "914389054085070854", + "chan_point": "bf91a203197160fc07cbb8bc9841a9d9027d01f00ad848cd2b51fba568eee726:6", + "last_update": 1710795190, + "capacity": "3000000", "node1_policy": { "time_lock_delta": 144, "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", "disabled": false, - "max_htlc_msat": "83955680000", - "last_update": 1710777182, + "max_htlc_msat": "2970000000", + "last_update": 1710789943, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 222, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "2000", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "83955680000", - "last_update": 1710785354, + "max_htlc_msat": "2970000000", + "last_update": 1710795190, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", - "node2_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + "custom_records": {} }, { - "channel_id": "889215735335550977", - "chan_point": "6dafd3c899572ba7c9cb984fa9108c8a7eb1a93e46df25966fc17090f4a240c2:1", - "last_update": 1710776050, - "capacity": "1000000", + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03c41fde73d7853d07c0b0baf110e91c5f54f768cc94d58ddc0d3e947fa7d27e29", + "channel_id": "914389054085070856", + "chan_point": "bf91a203197160fc07cbb8bc9841a9d9027d01f00ad848cd2b51fba568eee726:8", + "last_update": 1710829389, + "capacity": "3000000", "node1_policy": { - "time_lock_delta": 140, - "min_htlc": "100", - "fee_base_msat": "2000", - "fee_rate_milli_msat": "2500", + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "990000000", - "last_update": 1710776050, + "max_htlc_msat": "3000000000", + "last_update": 1710829389, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 222, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "2000", + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "250", "disabled": false, - "max_htlc_msat": "990000000", - "last_update": 1710772753, + "max_htlc_msat": "2970000000", + "last_update": 1710441695, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", - "node2_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + "custom_records": {} }, { - "channel_id": "861583908627218433", - "chan_point": "21c74ee01ff0eda80631adcb10469619adecd70f3d937d27d4fec60f0225019d:1", - "last_update": 1710856349, + "node1_pub": "021d2436cab847373a4212bf6d754ead5304f5d0791479643893a837b295f3441c", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "914389054085070859", + "chan_point": "bf91a203197160fc07cbb8bc9841a9d9027d01f00ad848cd2b51fba568eee726:11", + "last_update": 1710805990, "capacity": "3000000", "node1_policy": { - "time_lock_delta": 80, + "time_lock_delta": 100, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "159", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "144", "disabled": false, "max_htlc_msat": "2970000000", - "last_update": 1710856349, + "last_update": 1710786987, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 222, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "2000", + "fee_rate_milli_msat": "1", "disabled": false, "max_htlc_msat": "2970000000", - "last_update": 1710795398, + "last_update": 1710805990, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", - "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + "custom_records": {} }, { - "channel_id": "907088297038446592", - "chan_point": "6a5795e372ac56b4555dcc179a1f125d39f2f48800f079cd3ef8584cafae4817:0", - "last_update": 1710848353, - "capacity": "100000000", + "node1_pub": "03a21a8756ffc55dd1d83a02b0cc2e163f8d041e3f56b309232e5389c66c00364c", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "914423139045998595", + "chan_point": "57096610ffbee253cdc5ac665f9d68ff1e01e3744208dbe585aafbbbb06845f5:3", + "last_update": 1710849107, + "capacity": "5000000", "node1_policy": { - "time_lock_delta": 144, - "min_htlc": "1", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "999", + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710774858, + "max_htlc_msat": "4950000000", + "last_update": 1710785160, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 222, - "min_htlc": "1000", + "time_lock_delta": 100, + "min_htlc": "100000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "2000", - "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710848353, + "fee_rate_milli_msat": "656", + "disabled": true, + "max_htlc_msat": "4950000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "737654654585667584", - "chan_point": "0bebc81ceefe54c2568e4b762f2878eb4e7a85514f9346ba4352034521447269:0", - "last_update": 1710812050, - "capacity": "2000000", + "node1_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "914530891105763329", + "chan_point": "9cae8075084090c7a5fe2bdd2ab769448df70e2d764775b539286048fe2d30a7:1", + "last_update": 1710809735, + "capacity": "50000000", "node1_policy": { - "time_lock_delta": 37, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1700", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710786335, + "max_htlc_msat": "49500000000", + "last_update": 1710809735, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 140, + "time_lock_delta": 100, "min_htlc": "1000", - "fee_base_msat": "2000", - "fee_rate_milli_msat": "2500", + "fee_base_msat": "0", + "fee_rate_milli_msat": "693", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710812050, + "max_htlc_msat": "49500000000", + "last_update": 1710785638, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", - "node2_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + "custom_records": {} }, { - "channel_id": "894843035960147968", - "chan_point": "cfcfe8f1dc8cf0f20a6292facfc00d7e820e89cff3de64bd057801fc3ec3fc8d:0", - "last_update": 1710757535, - "capacity": "100000000", + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "channel_id": "914557279499714560", + "chan_point": "67feffbc619088def18fd128a3495fa1847adc6137f7f4f593f8d57652ecdee2:0", + "last_update": 1710845485, + "capacity": "5000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710757535, + "max_htlc_msat": "4950000000", + "last_update": 1710845485, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, + "time_lock_delta": 222, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", + "fee_rate_milli_msat": "2000", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710312917, + "max_htlc_msat": "4950000000", + "last_update": 1710826753, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "884413068699238401", - "chan_point": "92dcfce24fc43ea96273efbe4304ea5a2f34c69ab8067d06bea37b3436926cad:1", - "last_update": 1710816935, - "capacity": "550000", + "node1_pub": "031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "channel_id": "914557279499714563", + "chan_point": "67feffbc619088def18fd128a3495fa1847adc6137f7f4f593f8d57652ecdee2:3", + "last_update": 1710845481, + "capacity": "5000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "544500000", - "last_update": 1710816935, + "max_htlc_msat": "4950000000", + "last_update": 1710827735, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "544500000", - "last_update": 1710801275, + "max_htlc_msat": "4950000000", + "last_update": 1710845481, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", - "node2_pub": "03ea17fd97ca7382a192dd4533c8e95ca946ef0827b14984e682af699bcaeb5a53" + "custom_records": {} }, { - "channel_id": "892859516987572224", - "chan_point": "726039ab50c25e38a2df12dc67394879c36bb8420dc481b843d0833c8ee93303:0", - "last_update": 1710835918, - "capacity": "2000000", + "node1_pub": "026165850492521f4ac8abd9bd8088123446d126f648ca35e60f88177dc149ceb2", + "node2_pub": "026f46207fd290a33cbd86e29b3ad0a47cdd44ab9aa5267cde66483e10aa9d3180", + "channel_id": "914875038317084673", + "chan_point": "f44eb66f7166eba4d9bf82e0e95a78a429d180129d2145514235637907126b74:1", + "last_update": 1710855749, + "capacity": "30000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", + "fee_base_msat": "0", "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710802736, + "max_htlc_msat": "29700000000", + "last_update": 1710855749, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 80, + "time_lock_delta": 100, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "97", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710835918, + "max_htlc_msat": "29700000000", + "last_update": 1710799752, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000", - "node2_pub": "0328946a0693c2a4dd7e5afc2ce2e020c54e07d6693a293b4e1c1fd7e2d09a3eeb" + "custom_records": {} }, { - "channel_id": "907853557235646464", - "chan_point": "72d24dea72f3e930a219cfccfa352cc10040d1bcf49db365025d263e82c7de83:0", - "last_update": 1710776927, - "capacity": "50000", + "node1_pub": "0339be993760b0bfa4d45f775922d07992d19d9ba77faa4af6993d71e10404065c", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "channel_id": "914952004175986689", + "chan_point": "3e71151953a626e025a0a5f5341c41258b185dd3e1fb47c81a58bcd6f6ccfabe:1", + "last_update": 1710853832, + "capacity": "2500000", "node1_policy": { - "time_lock_delta": 80, + "time_lock_delta": 100, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "50", + "fee_rate_milli_msat": "2463", "disabled": false, - "max_htlc_msat": "49500000", - "last_update": 1710776927, + "max_htlc_msat": "2475000000", + "last_update": 1710853832, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 80, + "time_lock_delta": 222, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "2000", "disabled": false, - "max_htlc_msat": "49500000", - "last_update": 1710630223, + "max_htlc_msat": "2475000000", + "last_update": 1710817753, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03919a0a495cfd08779a3c23168827243cabe597e8ee5ea0c7827b6c407e260fe2", - "node2_pub": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac" + "custom_records": {} }, { - "channel_id": "763639412848394241", - "chan_point": "490f8ac59304fd951b06e6b56e07fba5623ccf567ed5ec0aa0f00f994a80995d:1", - "last_update": 1710846930, - "capacity": "100000", + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "030a425f5c69a29db30f6740d4e7df8f5612ef9955078ef4497490015464733dc8", + "channel_id": "915499560920612865", + "chan_point": "f75a0832dd73745124b0bea578081958ab5e8cbf65f0ac1d2dcd9e0bf84c43b9:1", + "last_update": 1710849820, + "capacity": "10000000", "node1_policy": { "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1", - "fee_rate_milli_msat": "100", + "min_htlc": "100", + "fee_base_msat": "0", + "fee_rate_milli_msat": "71", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1709509568, + "max_htlc_msat": "9801000000", + "last_update": 1710817203, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "10", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", "disabled": false, - "max_htlc_msat": "99000000", - "last_update": 1710846930, + "max_htlc_msat": "9900000000", + "last_update": 1710849820, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac", - "node2_pub": "03b9de7a7d7e8a474b859a59e314cf2a143ae9c569ce78c46289b3ad16cb8c03f9" + "custom_records": {} }, { - "channel_id": "855546490317897729", - "chan_point": "e1998ecf9d5bdf045a2b2e0fc868cb66532bc8e3c3910fac54039cb72e7b666b:1", - "last_update": 1710789782, - "capacity": "1100000", + "node1_pub": "0326e692c455dd554c709bbb470b0ca7e0bb04152f777d1445fd0bf3709a2833a3", + "node2_pub": "03d1e5e7e2ea43762f5e1afacfb60a8002ae57189eedcc13fcab44a084f7665d1f", + "channel_id": "915772239817867266", + "chan_point": "161bc2351f2ed836de0e4d638c46ee090e6795b421a0968aac46ad71aa1fc856:2", + "last_update": 1710849107, + "capacity": "7300000", "node1_policy": { - "time_lock_delta": 200, + "time_lock_delta": 120, "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "2000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "598", "disabled": false, - "max_htlc_msat": "1089000000", - "last_update": 1710789782, + "max_htlc_msat": "7227000000", + "last_update": 1710794158, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", + "time_lock_delta": 100, + "min_htlc": "100000", "fee_base_msat": "0", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "202", "disabled": false, - "max_htlc_msat": "1089000000", - "last_update": 1710777136, + "max_htlc_msat": "7227000000", + "last_update": 1710849107, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3", - "node2_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + "custom_records": {} }, { - "channel_id": "817245002822582272", - "chan_point": "4f774c8da0276be4aa4a488bff1877f1f0d42ce6d919b294d401ae9fceb5402b:0", - "last_update": 1710845182, - "capacity": "60000", + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "channel_id": "916338488352964608", + "chan_point": "4d0be2d98f261a3856dc53275711041441f11f517f0badf8731a3e926dac9de6:0", + "last_update": 1710786654, + "capacity": "50000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "5000", "disabled": false, - "max_htlc_msat": "59400000", - "last_update": 1710845182, + "max_htlc_msat": "20000000000", + "last_update": 1710786654, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "100", + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "699", "disabled": false, - "max_htlc_msat": "59400000", - "last_update": 1710775336, + "max_htlc_msat": "20000000000", + "last_update": 1710779520, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3", - "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + "custom_records": {} }, { - "channel_id": "858923090495012865", - "chan_point": "6ecf4b8b50ffcaa4180c686d37ac2f7d5208a385b3ff6cdfacd7bd49b0795716:1", - "last_update": 1710780782, - "capacity": "650000", + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "channel_id": "916409956472061954", + "chan_point": "e996c7149a97620ad97aed0c5f541874881ad8f847533f8776d78badf02867d4:2", + "last_update": 1710846600, + "capacity": "1000000", "node1_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "313", "disabled": false, - "max_htlc_msat": "643500000", - "last_update": 1710775476, + "max_htlc_msat": "990000000", + "last_update": 1710846600, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "643500000", - "last_update": 1710780782, + "max_htlc_msat": "990000000", + "last_update": 1710836407, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f", - "node2_pub": "0209a06852f1257d70e5cea44a0919871fa20859eb33a62445f774e9fe96247a75" + "custom_records": {} }, { - "channel_id": "837860845868220417", - "chan_point": "3188e2c87379e1784b904fc4ee2f6e75d846d87a9b8dd1db0e5217c11d8ce4ea:1", - "last_update": 1710775382, - "capacity": "3500000", + "node1_pub": "038bcc6471941c7d6c14a8ce5b7159d8f73a3a0bb9a93e8896bcb892e14552658a", + "node2_pub": "02b6dabd436275044399002241195b82b7fed517b226d0a109b1d07a39d7b4a91a", + "channel_id": "916409956472061956", + "chan_point": "e996c7149a97620ad97aed0c5f541874881ad8f847533f8776d78badf02867d4:4", + "last_update": 1710846600, + "capacity": "2000000", "node1_policy": { - "time_lock_delta": 144, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "500", - "fee_rate_milli_msat": "1000", - "disabled": true, - "max_htlc_msat": "3465000000", - "last_update": 1710775382, + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710806734, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "213", "disabled": false, - "max_htlc_msat": "3465000000", - "last_update": 1710537660, + "max_htlc_msat": "1980000000", + "last_update": 1710846600, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f", - "node2_pub": "033f481fe0e9344228b58e0297162bfa8d648d5043c12b6323df5eac61bd39094c" + "custom_records": {} }, { - "channel_id": "861833497883574273", - "chan_point": "0482621aeb0de1762640b99dfc600326903a52d11c8dcdf02a5e5d2652732c8f:1", - "last_update": 1710843782, - "capacity": "20000000", + "node1_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "916437444409950209", + "chan_point": "9c167b7ea19e3e411fdb05efb853a088927f211497a3efedc634c876776d1364:1", + "last_update": 1710818091, + "capacity": "90000000", "node1_policy": { - "time_lock_delta": 144, - "min_htlc": "1000", + "time_lock_delta": 140, + "min_htlc": "100", "fee_base_msat": "2000", - "fee_rate_milli_msat": "4000", + "fee_rate_milli_msat": "2500", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710843782, + "max_htlc_msat": "89100000000", + "last_update": 1710818091, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, - "min_htlc": "1", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", + "time_lock_delta": 100, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "504", "disabled": false, - "max_htlc_msat": "19800000000", - "last_update": 1710773696, + "max_htlc_msat": "89100000000", + "last_update": 1710789238, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "840309458235949057", - "chan_point": "012e468c6298ff7a689d671bfca97387e45ab7a7b6e7fc3dbfa46e55a95c6b59:1", - "last_update": 1710825499, - "capacity": "2000000", + "node1_pub": "03bc072a0a571b7635ffafcdec53da5c809d06f2f0418adfe41b40ff257e796107", + "node2_pub": "03aefa43fbb4009b21a4129d05953974b7dbabbbfb511921410080860fca8ee1f0", + "channel_id": "916512211176128513", + "chan_point": "0f59f910e580db96927ed45af05dd4eec3230a749384e6ed516d271f91bfff37:1", + "last_update": 1710854279, + "capacity": "5889000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", + "time_lock_delta": 34, + "min_htlc": "1", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "10", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710825118, + "max_htlc_msat": "5830110000", + "last_update": 1709812621, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, - "min_htlc": "10000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "90", + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "700", "disabled": false, - "max_htlc_msat": "1980000000", - "last_update": 1710825499, + "max_htlc_msat": "5889000000", + "last_update": 1710854279, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0", - "node2_pub": "0328946a0693c2a4dd7e5afc2ce2e020c54e07d6693a293b4e1c1fd7e2d09a3eeb" + "custom_records": {} }, { - "channel_id": "826733788095447041", - "chan_point": "dffc24350df473a58d4715c31f8ed35a7440387dbf59e16895a01a79c3d64ce2:1", - "last_update": 1710805699, - "capacity": "1000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "03a465772d45616bf6c8450a69191db8f3cf8cca19ff92138735fd5f1d436fe4dc", + "channel_id": "916526504728199168", + "chan_point": "6df1e8b752327328a4bc11065c820ec3465b17c606d9b655b1ec17ac929168d0:0", + "last_update": 1710772435, + "capacity": "15000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "10000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "90", + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "1000000000", - "last_update": 1710805699, + "max_htlc_msat": "15000000000", + "last_update": 1710772435, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, + "time_lock_delta": 34, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", "disabled": false, - "max_htlc_msat": "800000000", - "last_update": 1710798104, + "max_htlc_msat": "14850000000", + "last_update": 1710681712, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "909819483881668608", - "chan_point": "735fca87374ceba0dff8adec0bffafb1a870638f79f2b44246f1686644d8cdc9:0", - "last_update": 1710783381, - "capacity": "200000", + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "029267159e8ed64dc44f1deda34c918f45c7ba2d6b2533a2c1083a2d15f5f4330a", + "channel_id": "917082857725362177", + "chan_point": "137184a0412be2a6fb6f6bc7d65b00bac727120e7e68e01d6b02da091e5706e9:1", + "last_update": 1710855565, + "capacity": "6000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "30", - "disabled": true, - "max_htlc_msat": "198000000", - "last_update": 1710783323, + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710855565, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "100", - "fee_rate_milli_msat": "100", - "disabled": true, - "max_htlc_msat": "198000000", - "last_update": 1710783381, + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "5940000000", + "last_update": 1710836589, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880", - "node2_pub": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + "custom_records": {} }, { - "channel_id": "900662750928764928", - "chan_point": "c5babe10624dc1f24fd3e20eb59d0e6dbfc7dedf1ea0eaccb70f55a7abf3b38c:0", - "last_update": 1710844661, - "capacity": "100000000", + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "917416009602236416", + "chan_point": "10f782b804347657cdb925866a7b9616f15dd8fd203b3de709245ecc808939a7:0", + "last_update": 1710785691, + "capacity": "99995000", "node1_policy": { "time_lock_delta": 140, "min_htlc": "100", @@ -5908,283 +23643,281 @@ "fee_rate_milli_msat": "2500", "disabled": false, "max_htlc_msat": "20000000000", - "last_update": 1710844661, + "last_update": 1710785691, "custom_records": {} }, "node2_policy": { "time_lock_delta": 144, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "999", + "fee_rate_milli_msat": "499", "disabled": false, "max_htlc_msat": "20000000000", - "last_update": 1710009152, + "last_update": 1710265468, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "838462278610059264", - "chan_point": "6a7091a5715eda464ead722f2acbc4c5c1aeddab5fd4ae44c1ae9025b7f1d6d2:0", - "last_update": 1710846461, - "capacity": "50000000", + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "02b705ad9fff5b30e69dd3810d100372332c1a750db5a50edf7353c77ab486643e", + "channel_id": "918004248425529346", + "chan_point": "38ed9f1a163689c8cf6b9a9c4350c9416c3965c952a7fade6c3b70bb1fa9b282:2", + "last_update": 1710856207, + "capacity": "5000000", "node1_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "0", + "fee_rate_milli_msat": "150", "disabled": false, - "max_htlc_msat": "49500000000", - "last_update": 1710846461, + "max_htlc_msat": "4950000000", + "last_update": 1710835513, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "0", + "fee_rate_milli_msat": "425", "disabled": false, - "max_htlc_msat": "49500000000", - "last_update": 1710771291, + "max_htlc_msat": "4950000000", + "last_update": 1710856207, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149", - "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + "custom_records": {} }, { - "channel_id": "846395255002038273", - "chan_point": "ee07f74fc67ad1534106327880fae377fe91c9c7776f9d46699d28f99e9684c5:1", - "last_update": 1710848050, - "capacity": "10000000", + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "02324b351af4ff9afd9cff43abd1b46a7621412313f3c97e486af3e9f9dabb1535", + "channel_id": "918004248425529349", + "chan_point": "38ed9f1a163689c8cf6b9a9c4350c9416c3965c952a7fade6c3b70bb1fa9b282:5", + "last_update": 1710775604, + "capacity": "5000000", "node1_policy": { "time_lock_delta": 80, - "min_htlc": "1000", + "min_htlc": "1", "fee_base_msat": "0", "fee_rate_milli_msat": "0", - "disabled": false, - "max_htlc_msat": "9900000000", - "last_update": 1710788861, + "disabled": true, + "max_htlc_msat": "1000", + "last_update": 1710775604, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "0", + "fee_rate_milli_msat": "75", "disabled": false, - "max_htlc_msat": "9900000000", - "last_update": 1710848050, + "max_htlc_msat": "4950000000", + "last_update": 1710773407, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149", - "node2_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + "custom_records": {} }, { - "channel_id": "898725411427385345", - "chan_point": "96fe24d1c0ae58e89df72f3a0898f2bacc0dd58d11938325a54c46ad7b728804:1", - "last_update": 1710856837, - "capacity": "1000000", + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "channel_id": "918004248425529357", + "chan_point": "38ed9f1a163689c8cf6b9a9c4350c9416c3965c952a7fade6c3b70bb1fa9b282:13", + "last_update": 1710856207, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 50, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "10", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", "disabled": false, - "max_htlc_msat": "50000000", - "last_update": 1710856837, + "max_htlc_msat": "9900000000", + "last_update": 1710798790, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "600", + "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "990000000", - "last_update": 1710832983, + "max_htlc_msat": "9900000000", + "last_update": 1710856207, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02841286ba5314ac9a4a29eb8d558b30fec348f22e22351ff8896288dede68e6a7", - "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + "custom_records": {} }, { - "channel_id": "910155934451761152", - "chan_point": "888c6225644dc0f6fa8d22807a3cf9acb99bc1558806e6b2d5f15dad6cacfb59:0", - "last_update": 1710834571, - "capacity": "250000", + "node1_pub": "03e81689bfd18d0accb28d720ed222209b1a5f2c6825308772beac75b1fe35d491", + "node2_pub": "03b006c37dfb8681e5db9f513386d06c9d18bd514fae5d79a7ed2d5991c7d57330", + "channel_id": "918004248425529358", + "chan_point": "38ed9f1a163689c8cf6b9a9c4350c9416c3965c952a7fade6c3b70bb1fa9b282:14", + "last_update": 1710811804, + "capacity": "10000000", "node1_policy": { - "time_lock_delta": 50, + "time_lock_delta": 40, "min_htlc": "1000", - "fee_base_msat": "100", - "fee_rate_milli_msat": "10", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "99", "disabled": false, - "max_htlc_msat": "50000000", - "last_update": 1710782196, + "max_htlc_msat": "9900000000", + "last_update": 1710811804, "custom_records": {} }, "node2_policy": { "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "30", + "fee_rate_milli_msat": "160", "disabled": false, - "max_htlc_msat": "247500000", - "last_update": 1710834571, + "max_htlc_msat": "9900000000", + "last_update": 1710780607, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "02841286ba5314ac9a4a29eb8d558b30fec348f22e22351ff8896288dede68e6a7", - "node2_pub": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + "custom_records": {} }, { - "channel_id": "885818244503437313", - "chan_point": "0a0ac608d703bb383fd51e2ef0eebfb40e737d916f4e7810785ec8b9f7915d4e:1", - "last_update": 1710611588, - "capacity": "700000", + "node1_pub": "035e4ff418fc8b5554c5d9eea66396c227bd429a3251c8cbc711002ba215bfc226", + "node2_pub": "023e24602891c28a7872ea1ad5c1bb41abe4206ae1599bb981e3278a121e7895d6", + "channel_id": "918015243549736961", + "chan_point": "42dbf34ea726dc1e64e803aa04c65000268f4a5b53ebfdc2f6b4b201b63ab400:1", + "last_update": 1710841049, + "capacity": "20000000", "node1_policy": { - "time_lock_delta": 40, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "time_lock_delta": 144, + "min_htlc": "1000000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "750", "disabled": false, - "max_htlc_msat": "315000000", - "last_update": 1710558538, + "max_htlc_msat": "1980000000", + "last_update": 1710782702, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, - "min_htlc": "1", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", - "disabled": true, - "max_htlc_msat": "315000000", - "last_update": 1710611588, + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "300", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710841049, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "033f481fe0e9344228b58e0297162bfa8d648d5043c12b6323df5eac61bd39094c", - "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + "custom_records": {} }, { - "channel_id": "899068459022417920", - "chan_point": "f8751c293cfefc7e80bb4ae6c452ce17d33541ef420081b913ffc985ec246c16:0", - "last_update": 1710780553, - "capacity": "51000", + "node1_pub": "037f990e61acee8a7697966afd29dd88f3b1f8a7b14d625c4f8742bd952003a590", + "node2_pub": "0322d0e43b3d92d30ed187f4e101a9a9605c3ee5fc9721e6dac3ce3d7732fbb13e", + "channel_id": "918075716598693888", + "chan_point": "8a5823d42061f70ee8dbcc00ffb0d3920f3fa8e06afb58814e8034eb09f9f436:0", + "last_update": 1710832438, + "capacity": "50000000", "node1_policy": { - "time_lock_delta": 80, + "time_lock_delta": 144, "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1575", "disabled": false, - "max_htlc_msat": "50490000", - "last_update": 1700598649, + "max_htlc_msat": "49500000000", + "last_update": 1710832438, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", - "fee_base_msat": "1234", + "fee_base_msat": "1000", "fee_rate_milli_msat": "1", - "disabled": true, - "max_htlc_msat": "50490000", - "last_update": 1710780553, + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710802528, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0321743efb2d44a9c1ff8194f03728e119bbf6cde88201b9ac8e6e48f96c99d597", - "node2_pub": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43" + "custom_records": {} }, { - "channel_id": "846395255027269632", - "chan_point": "7fc299cd258401fef62f746434cefa05d42cd8895eec4f65d435ec2cd3bb3ec5:0", - "last_update": 1710848050, - "capacity": "20000000", + "node1_pub": "034d7f4bbbd6c1c1d8fbe0a42dd1f59e10b66540c6872dfcaa095d8d5cffebcf46", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "918152682457726977", + "chan_point": "faae42340d44bf90b3d0a7b0b313cd412e1bc8517ec330c3d2adb2541b06f261:1", + "last_update": 1710846891, + "capacity": "60150670", "node1_policy": { - "time_lock_delta": 80, + "time_lock_delta": 18, "min_htlc": "1000", "fee_base_msat": "0", "fee_rate_milli_msat": "0", "disabled": false, - "max_htlc_msat": "19800000000", - "last_update": 1710787491, + "max_htlc_msat": "59549164000", + "last_update": 1710846891, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 80, + "time_lock_delta": 20, "min_htlc": "1000", "fee_base_msat": "0", - "fee_rate_milli_msat": "0", + "fee_rate_milli_msat": "800", "disabled": false, - "max_htlc_msat": "19800000000", - "last_update": 1710848050, + "max_htlc_msat": "59549164000", + "last_update": 1710779404, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83", - "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + "custom_records": {} }, { - "channel_id": "917416009602236416", - "chan_point": "10f782b804347657cdb925866a7b9616f15dd8fd203b3de709245ecc808939a7:0", - "last_update": 1710785691, - "capacity": "99995000", + "node1_pub": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e", + "channel_id": "918390177017757696", + "chan_point": "d46c48c0d9410ab9597860b8ba10c14ed4c1d86338f80a7f7ff36ecf881269b3:0", + "last_update": 1710806052, + "capacity": "16770000", "node1_policy": { - "time_lock_delta": 140, - "min_htlc": "100", - "fee_base_msat": "2000", - "fee_rate_milli_msat": "2500", + "time_lock_delta": 72, + "min_htlc": "1000", + "fee_base_msat": "2147483647", + "fee_rate_milli_msat": "2147483647", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710785691, + "max_htlc_msat": "15093000000", + "last_update": 1710804699, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 144, - "min_htlc": "1000", - "fee_base_msat": "1000", - "fee_rate_milli_msat": "499", + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", "disabled": false, - "max_htlc_msat": "20000000000", - "last_update": 1710265468, + "max_htlc_msat": "1677000000", + "last_update": 1710806052, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", - "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + "custom_records": {} }, { - "channel_id": "865735664596090880", - "chan_point": "317d529be3e15cccad634f20811a98c2ba686188b9cad9803a322e8b29c5e4bc:0", - "last_update": 1710845901, - "capacity": "75000", + "node1_pub": "03abf6f44c355dec0d5aa155bdbdd6e0c8fefe318eff402de65c6eb2e1be55dc3e", + "node2_pub": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17", + "channel_id": "918393475539337217", + "chan_point": "b4cc302ae8d05a73006fb00cb873bb7ff39b521441abba49719139a1c539aabd:1", + "last_update": 1710841643, + "capacity": "16770000", "node1_policy": { - "time_lock_delta": 80, + "time_lock_delta": 72, "min_htlc": "1000", - "fee_base_msat": "0", - "fee_rate_milli_msat": "1", + "fee_base_msat": "2147483647", + "fee_rate_milli_msat": "2147483647", "disabled": false, - "max_htlc_msat": "74250000", - "last_update": 1710845901, + "max_htlc_msat": "15093000000", + "last_update": 1710804699, "custom_records": {} }, "node2_policy": { - "time_lock_delta": 40, + "time_lock_delta": 80, "min_htlc": "1000", "fee_base_msat": "1000", - "fee_rate_milli_msat": "1", + "fee_rate_milli_msat": "1000", "disabled": false, - "max_htlc_msat": "74250000", - "last_update": 1710845871, + "max_htlc_msat": "1677000000", + "last_update": 1710841643, "custom_records": {} }, - "custom_records": {}, - "node1_pub": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43", - "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + "custom_records": {} } ] } \ No newline at end of file diff --git a/test/data/LN_50.json b/test/data/LN_50.json new file mode 100644 index 000000000..2087c74d8 --- /dev/null +++ b/test/data/LN_50.json @@ -0,0 +1,6190 @@ +{ + "directed": false, + "multigraph": false, + "graph": {}, + "nodes": [ + { + "last_update": 1710607733, + "alias": "BlueWave", + "addresses": [ + { + "network": "tcp", + "addr": "bh32yyu7dgyodzq3wdunho5esew5qbvrd2flsynqzeyilnd7jnyeahqd.onion:9735" + } + ], + "color": "#d6de37", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6" + }, + { + "last_update": 1702845433, + "alias": "03796678b7111abef10f", + "addresses": [ + { + "network": "tcp", + "addr": "kgrxo24cm2ykhfazqer7bxzezcevrmir2du455o4mfbg3rpvqubpaqqd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03796678b7111abef10f3ff85b88f81f9cfe81cac7e3628a11af1679ed912757d5" + }, + { + "last_update": 1709578090, + "alias": "REDWHISPER", + "addresses": [ + { + "network": "tcp", + "addr": "piwsugtc6hr3jukavptmv6tjrxt2wt2i2q35lvjx6lk25vwvi43zicqd.onion:9735" + } + ], + "color": "#023bb9", + "features": { + "1": { + "name": "data-loss-protect", + "is_required": false, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { + "name": "static-remote-key", + "is_required": false, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "25": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "023bb9937dea9707583e45aa6708af0c5d16d9ca3970f67ab76606f43b7457309d" + }, + { + "last_update": 1710772374, + "alias": "fr33node", + "addresses": [ + { + "network": "tcp", + "addr": "ubzmye2tytcqarycr3jsirl3ajxallddfpvorc4t2odao4y5yovdalad.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02e45ec6486ad8d2b2dcfe3e994f89035f7f1975e800bfe56f030d910647601199" + }, + { + "last_update": 1710847816, + "alias": "LQwD-Canada", + "addresses": [ + { + "network": "tcp", + "addr": "192.243.215.102:9735" + }, + { + "network": "tcp", + "addr": "yit5nizyrgk4n2gx7hkadaas5iz33kn6wulr5vdndzpbae6tykqdw7ad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1" + }, + { + "last_update": 1710855669, + "alias": "River Financial 2", + "addresses": [ + { + "network": "tcp", + "addr": "34.136.230.235:9735" + } + ], + "color": "#ff9900", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5" + }, + { + "last_update": 1707020283, + "alias": "027d5369f807773ed75d", + "addresses": [ + { + "network": "tcp", + "addr": "leb7bxhf3ppdfssy2bu4udc2cnhbehw5nhkkiumh76fhxel6bqtqmqqd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "027d5369f807773ed75dc87dd12c585210b72827263f4a1009f052e67c12ac92f5" + }, + { + "last_update": 1710847881, + "alias": "lnd1.relampago.cash", + "addresses": [ + { + "network": "tcp", + "addr": "44.225.98.1:9735" + }, + { + "network": "tcp", + "addr": "6hmm7rvxv6mu7sv5wetqfigjljrde2cd6kr474ncylbbfzdsczyvhlad.onion:9735" + } + ], + "color": "#ffa47a", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d" + }, + { + "last_update": 1710623243, + "alias": "bitpi", + "addresses": [ + { + "network": "tcp", + "addr": "iddfruwmorrrlr32akea422ajghfyicyohdd3n3a6mmiigd5x7druhyd.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3" + }, + { + "last_update": 1674198664, + "alias": "Bitcomoon.com", + "addresses": [ + { + "network": "tcp", + "addr": "e3xx3u5oxvuo4bgix5cl2ngfeyvexoz7my3rktgrpmf6gq3sryi2orqd.onion:9735" + } + ], + "color": "#79e0ed", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02bf145fe009fef1230f3f435b5e448db3470a1fdbff95d41cefa71b7d6f14fcda" + }, + { + "last_update": 1710586588, + "alias": "Bitrequest", + "addresses": [ + { + "network": "tcp", + "addr": "wvhczhxeyuo5gsbc6ub5n66yxycvojyx26eunh76l3ta7ks7yjckbgqd.onion:9735" + } + ], + "color": "#0cc0ed", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296" + }, + { + "last_update": 1710773661, + "alias": "raspiblitz", + "addresses": [ + { + "network": "tcp", + "addr": "n75lmwb3ykmwvbqajzkdh6otc6ukasij4f556ub54ug4qvs22rb3rsyd.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02c87bce43398e73f65df561b4e776306fc1e74657d67131d76866ba617a0e63c2" + }, + { + "last_update": 1710825523, + "alias": "qubi1", + "addresses": [ + { + "network": "tcp", + "addr": "mkejl5jnqe4ovaow57l47hi46xbjcxnf54jriiwvs5rsxcxxjbo5b4yd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02b196899ff33d892562c557a37f56a3702f14eba8aee8585625ed8cf4217f2ab3" + }, + { + "last_update": 1710850518, + "alias": "Knoten", + "addresses": [ + { + "network": "tcp", + "addr": "xetu2kpbvonq2urqpadclyzy76gwhdrkz6lgazz65j66leapw4bmpoad.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96" + }, + { + "last_update": 1710568425, + "alias": "Viva Bitcoin", + "addresses": [ + { + "network": "tcp", + "addr": "2ti2gy74nlyjsgcs42wemjwd3skmndbbv776ufldkngbzmpo7tbsntyd.onion:9735" + } + ], + "color": "#ffff00", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02aeb397b7212dc1de5252684f9a1f1957d8bcc9864193b538ce78f9558685d666" + }, + { + "last_update": 1710394153, + "alias": "", + "addresses": [], + "color": "#007f00", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20" + }, + { + "last_update": 1710769806, + "alias": "EcoNode", + "addresses": [ + { + "network": "tcp", + "addr": "kpa3fv64oxsgl7ymsmu27t5wwe4vpx47wer3tr7fk5ateeskmoh6hiqd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0202b5d54467d075893d46b6205067bbab2bcbc632513cd15661ad803d2dcb4ce3" + }, + { + "last_update": 1707656697, + "alias": "02f5be5f3d54b66bf531", + "addresses": [ + { + "network": "tcp", + "addr": "vedvjiibjrbbaaegvjqvx4thfobbj7gs5scb5gtpvxyb3t423ao23sad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02f5be5f3d54b66bf531b96f06327f59cd139f6e2c301cc0131a3f632025c2704c" + }, + { + "last_update": 1710693115, + "alias": "lightning-roulette.com", + "addresses": [ + { + "network": "tcp", + "addr": "34.65.32.189:9735" + } + ], + "color": "#11ff22", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403" + }, + { + "last_update": 1662699597, + "alias": "BambergCityNode5", + "addresses": [ + { + "network": "tcp", + "addr": "tfsmtkznl6spgnm5j3xxa6wmawvmi4ipgbvuazdngy6xqbigqdiojpid.onion:9735" + } + ], + "color": "#cd6d5f", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "024a65932bdad1c4ef070289a6dd9f71b315e61d871b38c79339d0cc3d26e100e1" + }, + { + "last_update": 1710392585, + "alias": "", + "addresses": [], + "color": "#007f00", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + }, + "51": { + "name": "zero-conf", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17" + }, + { + "last_update": 1710817951, + "alias": "bblocker21", + "addresses": [ + { + "network": "tcp", + "addr": "l4nxfimtxjjerzewyb4ihp4jhiauw2madq3krdnqz4s3x52kmya76lyd.onion:9735" + } + ], + "color": "#ffb233", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8" + }, + { + "last_update": 1710847967, + "alias": "Bitrefill", + "addresses": [ + { + "network": "tcp", + "addr": "54.155.41.207:9735" + } + ], + "color": "#002b28", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + }, + { + "last_update": 1710767460, + "alias": "lnd.cryptoassets.co.za", + "addresses": [ + { + "network": "tcp", + "addr": "197.155.6.194:9735" + }, + { + "network": "tcp", + "addr": "xiigglrrhl6gl55g6ndou5pkg7y7gxmln6bs6eov2w5od2xavdfukrqd.onion:9735" + } + ], + "color": "#000000", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "021227d4be948ab84613d5dbd47b6e148cfb811432d27eb92e80d28382b1b27d27" + }, + { + "last_update": 1710821651, + "alias": "OpenNode.com", + "addresses": [ + { + "network": "tcp", + "addr": "18.222.70.85:9735" + } + ], + "color": "#000000", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + }, + { + "last_update": 1710836095, + "alias": "021cb32426ed1a6be953", + "addresses": [ + { + "network": "tcp", + "addr": "g2vldjytfdx4r2hdmgg3ccqrxczxgxl4e3w7rr3emvp4br4xcoo4void.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000" + }, + { + "last_update": 1710776023, + "alias": "Fastlightning", + "addresses": [ + { + "network": "tcp", + "addr": "tspdnxsdk77b6wdly6lujqr724hoq23buuez3roy6xo5ausv3riisbqd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03919a0a495cfd08779a3c23168827243cabe597e8ee5ea0c7827b6c407e260fe2" + }, + { + "last_update": 1710845779, + "alias": "LightningPremium", + "addresses": [ + { + "network": "tcp", + "addr": "5u3crvcbydktdcwsk5utapo5vvd7s2uzkn45u7kemusvgikwqydytjad.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac" + }, + { + "last_update": 1648917357, + "alias": "mynodebtc.cooper", + "addresses": [ + { + "network": "tcp", + "addr": "bh4ooqtd3mngj4vk3m3gpqd3isaywi67ed5iochdg3lhexzikc56epad.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03d213f4115a55f0d0ab185434fcb6d3f119fdede11e901fe57f8b91d57ed316e6" + }, + { + "last_update": 1710852615, + "alias": "Decentralized", + "addresses": [ + { + "network": "tcp", + "addr": "sgcq35suws6ft63egprwsncj357yt355rsa3aqqpjjimma4nfjhzdyqd.onion:9735" + } + ], + "color": "#61f5b2", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3" + }, + { + "last_update": 1710551722, + "alias": "02abb5e57ff442770d9d", + "addresses": [ + { + "network": "tcp", + "addr": "172.67.158.44:9735" + }, + { + "network": "tcp", + "addr": "rjufmivql2si355mfczbg2wirc24rnjngpo5hsotmabiafuewojkbdqd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02abb5e57ff442770d9dc1d1ab66f45b015ac7aa033e4407051d060a471b71b08b" + }, + { + "last_update": 1710852249, + "alias": "Bitrefill Routing", + "addresses": [ + { + "network": "tcp", + "addr": "54.77.250.40:9735" + } + ], + "color": "#ff001c", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + }, + { + "last_update": 1710811679, + "alias": "bleskomat", + "addresses": [ + { + "network": "tcp", + "addr": "165.227.161.245:9735" + }, + { + "network": "tcp", + "addr": "5aggdumr4x3qkx5ptshggvrdkgaxkpsklw4nwmnsa6diby6vwcvccyad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0" + }, + { + "last_update": 1710852256, + "alias": "Liaoning", + "addresses": [ + { + "network": "tcp", + "addr": "vdhhkanmkfr7x2mlkbo7lah6sh7mhwxqt7gv73m44pkfk4y537tg54yd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880" + }, + { + "last_update": 1710848892, + "alias": "shtyrlitz", + "addresses": [ + { + "network": "tcp", + "addr": "aswabsuhftuqagrvxlhzc5la73wj5skg7shp4vjpwgialbcgkceby6qd.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0328946a0693c2a4dd7e5afc2ce2e020c54e07d6693a293b4e1c1fd7e2d09a3eeb" + }, + { + "last_update": 1710803334, + "alias": "FinanzielleFreiheit", + "addresses": [ + { + "network": "tcp", + "addr": "valdvygv5w6bfnza4yfshqsvljhv7grgyq4wgpke2jwvugfretiqwcqd.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03ea17fd97ca7382a192dd4533c8e95ca946ef0827b14984e682af699bcaeb5a53" + }, + { + "last_update": 1710805061, + "alias": "lndwr2.zaphq.io", + "addresses": [ + { + "network": "tcp", + "addr": "34.74.114.254:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149" + }, + { + "last_update": 1710856675, + "alias": "Civilization Phaze IV", + "addresses": [ + { + "network": "tcp", + "addr": "ko4qvn23p2txecujtt5nv67mb2ikzhbebh2hs4rgfvonfpk2tqhux4id.onion:9735" + } + ], + "color": "#ff8733", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "02841286ba5314ac9a4a29eb8d558b30fec348f22e22351ff8896288dede68e6a7" + }, + { + "last_update": 1710771876, + "alias": "0209a06852f1257d70e5", + "addresses": [ + { + "network": "tcp", + "addr": "94.130.220.96:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0209a06852f1257d70e5cea44a0919871fa20859eb33a62445f774e9fe96247a75" + }, + { + "last_update": 1710586096, + "alias": "PERLY", + "addresses": [ + { + "network": "tcp", + "addr": "twtvqk7jiz3luzkt346q326u5owmo73lfn4yhhr5efskkr4autuf6vqd.onion:9735" + } + ], + "color": "#9245ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "033f481fe0e9344228b58e0297162bfa8d648d5043c12b6323df5eac61bd39094c" + }, + { + "last_update": 1700638994, + "alias": "UmbrelMCL", + "addresses": [ + { + "network": "tcp", + "addr": "rlubaw7sxcaqmostf3e4x7yollfo4x2tzfwxrj3ssvxqusdes2q6k6yd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0321743efb2d44a9c1ff8194f03728e119bbf6cde88201b9ac8e6e48f96c99d597" + }, + { + "last_update": 1710736174, + "alias": "Laika", + "addresses": [ + { + "network": "tcp", + "addr": "tezwktlb2f3ah2tbla6emehwic4eelwyxhycfvzgzmc7n6jxz33ozead.onion:9735" + } + ], + "color": "#300fb0", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03b9de7a7d7e8a474b859a59e314cf2a143ae9c569ce78c46289b3ad16cb8c03f9" + }, + { + "last_update": 1710803050, + "alias": "lndeu0.zaphq.io", + "addresses": [ + { + "network": "tcp", + "addr": "35.196.134.164:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + }, + { + "last_update": 1710790037, + "alias": "0294774ee02a9faa5a58", + "addresses": [], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "0294774ee02a9faa5a5870061f7f4833686184ad14a0b163c49442516c9edac1db" + }, + { + "last_update": 1709977298, + "alias": "ACINQ", + "addresses": [ + { + "network": "tcp", + "addr": "3.33.236.230:9735" + }, + { + "network": "tcp", + "addr": "of7husrflx7sforh3fw6yqlpwstee3wg5imvvmkp4bz6rbjxtg5nljad.onion:9735" + } + ], + "color": "#49daaa", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "8": { + "name": "tlv-onion", + "is_required": true, + "is_known": true + }, + "11": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "13": { + "name": "static-remote-key", + "is_required": false, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "29": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "39": { + "name": "unknown", + "is_required": false, + "is_known": false + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "47": { + "name": "scid-alias", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "last_update": 1710849539, + "alias": "PeterJFrancoIII", + "addresses": [ + { + "network": "tcp", + "addr": "oyonslv2muhpngxl3yip3z7mt27auzlsx2luqydkxadludv2cva3thad.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + }, + { + "last_update": 1710850693, + "alias": "Wallet Sat's", + "addresses": [ + { + "network": "tcp", + "addr": "4a2tjhiyz3rqgpfvragnk4uszv32dytzhapen46igyjlyzfpgix5zeqd.onion:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43" + }, + { + "last_update": 1710617991, + "alias": "RUSHBNOSTOP", + "addresses": [ + { + "network": "tcp", + "addr": "maeiws6uq6baeuim5q6mcavn3xpn4kkpaa5rv7uqhrs6wubhccnkn7ad.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + }, + { + "last_update": 1710774982, + "alias": "sheesh", + "addresses": [ + { + "network": "tcp", + "addr": "esowgqg2jhjzofvgkwdnyrozdvowvx7ymo3jsdh7oxxc74y7ga4egyyd.onion:9735" + } + ], + "color": "#68f442", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "55": { + "name": "keysend", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + }, + { + "last_update": 1710855402, + "alias": "lndus0.zaphq.io", + "addresses": [ + { + "network": "tcp", + "addr": "34.138.228.220:9735" + } + ], + "color": "#3399ff", + "features": { + "0": { + "name": "data-loss-protect", + "is_required": true, + "is_known": true + }, + "5": { + "name": "upfront-shutdown-script", + "is_required": false, + "is_known": true + }, + "7": { + "name": "gossip-queries", + "is_required": false, + "is_known": true + }, + "9": { + "name": "tlv-onion", + "is_required": false, + "is_known": true + }, + "12": { + "name": "static-remote-key", + "is_required": true, + "is_known": true + }, + "14": { + "name": "payment-addr", + "is_required": true, + "is_known": true + }, + "17": { + "name": "multi-path-payments", + "is_required": false, + "is_known": true + }, + "19": { + "name": "wumbo-channels", + "is_required": false, + "is_known": true + }, + "23": { + "name": "anchors-zero-fee-htlc-tx", + "is_required": false, + "is_known": true + }, + "27": { + "name": "shutdown-any-segwit", + "is_required": false, + "is_known": true + }, + "31": { + "name": "amp", + "is_required": false, + "is_known": true + }, + "45": { + "name": "explicit-commitment-type", + "is_required": false, + "is_known": true + }, + "2023": { + "name": "script-enforced-lease", + "is_required": false, + "is_known": true + } + }, + "custom_records": {}, + "pub_key": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + } + ], + "edges": [ + { + "channel_id": "821685930207739904", + "chan_point": "b8d52e4be913496dd31204b80a6214d30934fd77cf18d33f1f4b6f2b17aabc0a:0", + "last_update": 1710852888, + "capacity": "130000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "128700000", + "last_update": 1710852888, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "128700000", + "last_update": 1710852885, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6", + "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + }, + { + "channel_id": "851482695379255296", + "chan_point": "57457180438919be15029ad7763d11eb46d33057fe9ab02ca001439b48350e97:0", + "last_update": 1710451930, + "capacity": "300000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "297000000", + "last_update": 1710451930, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": true, + "max_htlc_msat": "297000000", + "last_update": 1706955502, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6", + "node2_pub": "027d5369f807773ed75dc87dd12c585210b72827263f4a1009f052e67c12ac92f5" + }, + { + "channel_id": "852238059856199681", + "chan_point": "48442adc84d9f96ea82a15d24eb5eed6ea66a8b633296a7a0f7cd0adebbf4685:1", + "last_update": 1710705189, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710617530, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710705189, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0242902a3a5aa34829db9def5b44939f9f459f4ee08e97cba18516c62ddf8ec9e6", + "node2_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1" + }, + { + "channel_id": "892895800825741312", + "chan_point": "997c879c28e44b11493fffc1e3bdd41bfab8ebcb815a3b8b0c6d0f7baeabe86b:0", + "last_update": 1710607091, + "capacity": "50000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "49500000", + "last_update": 1710607091, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "49500000", + "last_update": 1705276253, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03796678b7111abef10f3ff85b88f81f9cfe81cac7e3628a11af1679ed912757d5", + "node2_pub": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000" + }, + { + "channel_id": "892768257448935424", + "chan_point": "7b2351fe1f9dc287e376f481ae3e52cb39ad417cb8387087b91054e300ddc486:0", + "last_update": 1710639306, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "2000000000", + "last_update": 1710607091, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 34, + "min_htlc": "1", + "fee_base_msat": "5805", + "fee_rate_milli_msat": "6", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710639306, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "023bb9937dea9707583e45aa6708af0c5d16d9ca3970f67ab76606f43b7457309d", + "node2_pub": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000" + }, + { + "channel_id": "915246673262739457", + "chan_point": "4c2e260ea115e813a1255e12a1597e2577216d37d770c3e2328170fc28ffb153:1", + "last_update": 1710772374, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710771876, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710772374, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02e45ec6486ad8d2b2dcfe3e994f89035f7f1975e800bfe56f030d910647601199", + "node2_pub": "0209a06852f1257d70e5cea44a0919871fa20859eb33a62445f774e9fe96247a75" + }, + { + "channel_id": "888979340321554436", + "chan_point": "0055cf02d8bbc7125784e8fee50605969c6cfe76fa24cea14e9ae66a6be4a7c8:4", + "last_update": 1710793392, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790557, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710793392, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8" + }, + { + "channel_id": "886619788490833928", + "chan_point": "fe88ddff03886631613cfb2bf0f80849f8074bb2f4269c05de8abcc110741e65:8", + "last_update": 1710791589, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710789935, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710791589, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + }, + { + "channel_id": "914391253219868685", + "chan_point": "acf8a90a8ba334f408b13a1e581538c6fcc3d2398293a59b5fcad77f26769ca2:13", + "last_update": 1710813189, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 200, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710789782, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710813189, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + }, + { + "channel_id": "886786914241085444", + "chan_point": "c34a46e8d7fd82170c9fc87cfcbc848c477a40799d638512bb60054aafd69cf3:4", + "last_update": 1710830106, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "159", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710828768, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710830106, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + }, + { + "channel_id": "890068956432236548", + "chan_point": "ffbc1367d23f03fc1b3551faf2832e16668ee135fc780e22e46b72fb025c05a1:4", + "last_update": 1710795190, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790263, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710795190, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d" + }, + { + "channel_id": "889913925275484176", + "chan_point": "48cd67b0995fb7e49876f4e18fba74bbde68bf3c713b6cc5ceb93c4f3272f4ee:16", + "last_update": 1710829389, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "10000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "90", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710789499, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710829389, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0" + }, + { + "channel_id": "888987036985851905", + "chan_point": "094ff6c4b2ee8473bf7839d6d48c389576209ef4c581abb7abcab755bd20a4b8:1", + "last_update": 1710791182, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710791182, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710775389, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + }, + { + "channel_id": "886894666356949005", + "chan_point": "638f26ea27dfba927dfb3897f9fe9489d604f12b8b7721eb4f76fe09896f9f01:13", + "last_update": 1710841936, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710841936, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710793390, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3" + }, + { + "channel_id": "889855651147612169", + "chan_point": "7ce4049dacc96cbed13afd0664d05d6d4f68ec7b6d1bd6181fe03736af188fea:9", + "last_update": 1710813189, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710813189, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710790753, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + }, + { + "channel_id": "890068956432236557", + "chan_point": "ffbc1367d23f03fc1b3551faf2832e16668ee135fc780e22e46b72fb025c05a1:13", + "last_update": 1710793392, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710793392, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710789318, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0364913d18a19c671bb36dd04d6ad5be0fe8f2894314c36a9db3f03c2d414907e1", + "node2_pub": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96" + }, + { + "channel_id": "918488033557348353", + "chan_point": "3c57def65d9db037ef262b94448e0b19413365d50874dadf6c8706704ef2a6a0:1", + "last_update": 1710855657, + "capacity": "16770000", + "node1_policy": { + "time_lock_delta": 72, + "min_htlc": "1", + "fee_base_msat": "2147483647", + "fee_rate_milli_msat": "2147483647", + "disabled": false, + "max_htlc_msat": "15093000000", + "last_update": 1710855657, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "1677000000", + "last_update": 1710855493, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", + "node2_pub": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17" + }, + { + "channel_id": "913633689575358465", + "chan_point": "f4ba21d0d25d0d064fab44160da431b099ffa8f7fe5a7d1289a4ed3d2ce3c60b:1", + "last_update": 1710822543, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710789935, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710822543, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + }, + { + "channel_id": "914491308634472449", + "chan_point": "a0e58782bcff1c828a74744932f8ab32bafc27b275001372c2e62b5955387602:1", + "last_update": 1710790143, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710790037, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "99000000000", + "last_update": 1710790143, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", + "node2_pub": "0294774ee02a9faa5a5870061f7f4833686184ad14a0b163c49442516c9edac1db" + }, + { + "channel_id": "918488033557217280", + "chan_point": "9fd21e0a421924545e30b891812695791696c676367094ffe2467460eff56c36:0", + "last_update": 1710855500, + "capacity": "16770000", + "node1_policy": { + "time_lock_delta": 72, + "min_htlc": "1", + "fee_base_msat": "2147483647", + "fee_rate_milli_msat": "2147483647", + "disabled": true, + "max_htlc_msat": "15093000000", + "last_update": 1710855462, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "1677000000", + "last_update": 1710855500, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", + "node2_pub": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20" + }, + { + "channel_id": "917417109119041536", + "chan_point": "9af4014c8a9dafc53ef2913a4bde5e2111cf10205c1905ae585e5511de108bcc:0", + "last_update": 1710854361, + "capacity": "99995000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710854361, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "750", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710822543, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "914489109751398401", + "chan_point": "c51bb911ae676ddcbd7207a596dbea51151ba678399287ab8221f27cf93fdbf2:1", + "last_update": 1710790753, + "capacity": "25000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710777543, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "24750000000", + "last_update": 1710790753, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03aab7e9327716ee946b8fbfae039b0db85356549e72c5cca113ea67893d0821e5", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + }, + { + "channel_id": "850763614782029825", + "chan_point": "595e98da49698fdb9d9e25fbe9279c7f15897193bb13da9faef8ef1213a13fa5:1", + "last_update": 1710609731, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1707058294, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "300", + "disabled": true, + "max_htlc_msat": "495000000", + "last_update": 1710609731, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "027d5369f807773ed75dc87dd12c585210b72827263f4a1009f052e67c12ac92f5", + "node2_pub": "02aeb397b7212dc1de5252684f9a1f1957d8bcc9864193b538ce78f9558685d666" + }, + { + "channel_id": "912038298208501760", + "chan_point": "42df48fd0764c7777e7e0f290e9538f3082222648941e6078c348f6f02a4457d:0", + "last_update": 1710855461, + "capacity": "6710669", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "6643563000", + "last_update": 1710855461, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "6643563000", + "last_update": 1710846063, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d", + "node2_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149" + }, + { + "channel_id": "891582983965769729", + "chan_point": "2dd71734bb100d72ae15439e6e707f6ca451040f21dc8f79df75d8fac835caf4:1", + "last_update": 1710855891, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710855891, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "500", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710847863, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0338bbdb38184852f728ef833e9ca0d01e134e9490f5b3fd3219b3ad9eaa0fc49d", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + }, + { + "channel_id": "854880186313605121", + "chan_point": "78dc9a67189d0d44d3678cb35bb8b297fde81743c3ebdfaa1651fe4ad731542e:1", + "last_update": 1710786746, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710770149, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710786746, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", + "node2_pub": "02abb5e57ff442770d9dc1d1ab66f45b015ac7aa033e4407051d060a471b71b08b" + }, + { + "channel_id": "781908898078588929", + "chan_point": "2d4c7735462b434dc79bd300e9d788cb78af0643c668531dab090c5d539fb3d1:1", + "last_update": 1710844710, + "capacity": "1500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "1500000000", + "last_update": 1710844710, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "400000000", + "last_update": 1710684918, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "793454869742813185", + "chan_point": "5327c98b3ce6a42c8baeebdc740bf24fbf0bedf5aa1f56eeebcc2b58fab5d54f:1", + "last_update": 1710791299, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "90", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710787110, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "90", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710791299, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", + "node2_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0" + }, + { + "channel_id": "835777271311630337", + "chan_point": "26bedad6cbd11dd2054557a51631971287141b0b487ef09e1482601e8f24f28f:1", + "last_update": 1710785354, + "capacity": "1620000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1603800000", + "last_update": 1710778110, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "1603800000", + "last_update": 1710785354, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "033e8bc45cc79dabebd18507ba39b101c839240932174bff6ddf34631305881cc3", + "node2_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac" + }, + { + "channel_id": "817806853236391936", + "chan_point": "2d7da2726a4724f8a350315840bd7e45b383c4e909e749b235d62339880b67fb:0", + "last_update": 1710791182, + "capacity": "90000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "89100000", + "last_update": 1710509231, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "89100000", + "last_update": 1710791182, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02bf145fe009fef1230f3f435b5e448db3470a1fdbff95d41cefa71b7d6f14fcda", + "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + }, + { + "channel_id": "885613735298072577", + "chan_point": "aae3c5dac5bc5a9b5813bb1f37592ca1aa0747d3b8089facdbf584d368ba94cb:1", + "last_update": 1710846929, + "capacity": "24639", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "24393000", + "last_update": 1710710327, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "24393000", + "last_update": 1710846929, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296", + "node2_pub": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac" + }, + { + "channel_id": "817627632791715840", + "chan_point": "349640c4fbb05af12ebc1bc904758dae19e90b3aa3ad2de840ad3b7c84889e56:0", + "last_update": 1710803782, + "capacity": "80000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "79200000", + "last_update": 1710775588, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "79200000", + "last_update": 1710803782, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296", + "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + }, + { + "channel_id": "868515230059724800", + "chan_point": "5c1b233a9778af0356a5e02dd8d758ebb8b85ddbcf8252d793217d1a28dedd67:0", + "last_update": 1710635188, + "capacity": "80000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "79200000", + "last_update": 1710635188, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "79200000", + "last_update": 1707066297, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02b568dfb3cb52a0bde61b706f333a4eb4b77b0d38f0a9a591a338a05ae5130296", + "node2_pub": "02f5be5f3d54b66bf531b96f06327f59cd139f6e2c301cc0131a3f632025c2704c" + }, + { + "channel_id": "909468739601629185", + "chan_point": "ca9c5d9014ff6701875ec69d4db910fb7592f39ed5ad44eac23ff1f1031267f2:1", + "last_update": 1710783381, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "167", + "disabled": true, + "max_htlc_msat": "495000000", + "last_update": 1710783379, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "100", + "disabled": true, + "max_htlc_msat": "495000000", + "last_update": 1710783381, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02c87bce43398e73f65df561b4e776306fc1e74657d67131d76866ba617a0e63c2", + "node2_pub": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880" + }, + { + "channel_id": "913193885013966849", + "chan_point": "087e6845eefd047bb0dd3ee591a2dbea28b504b8a07fd733f5eb51bf89b0ca24:1", + "last_update": 1710834571, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710779061, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "30", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710834571, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02c87bce43398e73f65df561b4e776306fc1e74657d67131d76866ba617a0e63c2", + "node2_pub": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + }, + { + "channel_id": "769006129141776384", + "chan_point": "e5ebf62a8e888892b505156f95c8c6c7cb60ecf9801ec9f6650d3f303e53cba9:0", + "last_update": 1710704923, + "capacity": "250000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "247500000", + "last_update": 1710531493, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "247500000", + "last_update": 1710704923, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02b196899ff33d892562c557a37f56a3702f14eba8aee8585625ed8cf4217f2ab3", + "node2_pub": "021227d4be948ab84613d5dbd47b6e148cfb811432d27eb92e80d28382b1b27d27" + }, + { + "channel_id": "770339836665790464", + "chan_point": "fd89d822c95341eb4948000720b9747ce4b517e3ec6726db5774bdda4f25164c:0", + "last_update": 1710773323, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710771935, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710773323, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02b196899ff33d892562c557a37f56a3702f14eba8aee8585625ed8cf4217f2ab3", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + }, + { + "channel_id": "860278788451991553", + "chan_point": "34f76ac4003f888ceeeeb9f71b3912ca731bebf78c3a808d3abfb4abd5beb96a:1", + "last_update": 1710851757, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710851757, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710817465, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96", + "node2_pub": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8" + }, + { + "channel_id": "865665295861612544", + "chan_point": "95a81b50d4b7055f0af96915ff3226e0d8b97d6a1e3d6975e02a295301468c8f:0", + "last_update": 1710594913, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710125353, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710594913, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03c0223a47deb4abe67071ad16f66c5c8dc38bbd5bd117589f80ba1d51eb7ddb96", + "node2_pub": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43" + }, + { + "channel_id": "850260038455197696", + "chan_point": "81b147059ab5ee28ccb9e1af5158254e6a0ceb3170ca1ebd2c062bad44ddf479:0", + "last_update": 1710798303, + "capacity": "5000223", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "4950221000", + "last_update": 1710798303, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "4950221000", + "last_update": 1710798303, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02aeb397b7212dc1de5252684f9a1f1957d8bcc9864193b538ce78f9558685d666", + "node2_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403" + }, + { + "channel_id": "918392376041340929", + "chan_point": "f494169e156a65a8cfb628bd036a591d659af6aadff5604c89c0faf7fc92d527:1", + "last_update": 1710841167, + "capacity": "16770000", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "1677000000", + "last_update": 1710841167, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 72, + "min_htlc": "1000", + "fee_base_msat": "2147483647", + "fee_rate_milli_msat": "2147483647", + "disabled": false, + "max_htlc_msat": "15093000000", + "last_update": 1710802200, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + }, + { + "channel_id": "918425361248288768", + "chan_point": "036f4a9be00df25cc8abfcdeeae828e0fbc7d82c92109e907aaf9837501680f7:0", + "last_update": 1710829005, + "capacity": "16770000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": true, + "max_htlc_msat": "1677000000", + "last_update": 1710829005, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 72, + "min_htlc": "1000", + "fee_base_msat": "2147483647", + "fee_rate_milli_msat": "2147483647", + "disabled": false, + "max_htlc_msat": "15093000000", + "last_update": 1710816411, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "031fab3f6a8ae8588668fbe4bf4cae14c3aaa4134330b1798b81e60aaf9662ff20", + "node2_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4" + }, + { + "channel_id": "763640512349143040", + "chan_point": "50ad5a9f0c31ec00e8e5dfc4a3947ea363542be0db57a538c014d1693c00782f:0", + "last_update": 1710398772, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "20", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1709879445, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710398772, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0202b5d54467d075893d46b6205067bbab2bcbc632513cd15661ad803d2dcb4ce3", + "node2_pub": "03b9de7a7d7e8a474b859a59e314cf2a143ae9c569ce78c46289b3ad16cb8c03f9" + }, + { + "channel_id": "908048170593550337", + "chan_point": "0d45ab108774a44b8ed9a0a09c6ae6ac6390447c8c4f31762c9984e268dbf63a:1", + "last_update": 1710856405, + "capacity": "500000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710856405, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "495000000", + "last_update": 1710801772, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0202b5d54467d075893d46b6205067bbab2bcbc632513cd15661ad803d2dcb4ce3", + "node2_pub": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880" + }, + { + "channel_id": "756393631196446720", + "chan_point": "f64829c1e94063373474fc4be07bb1e594b4fd639be17b437af1d84fa1c629c1:0", + "last_update": 1710811915, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1662603210, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "750", + "disabled": true, + "max_htlc_msat": "990000000", + "last_update": 1710811915, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403", + "node2_pub": "024a65932bdad1c4ef070289a6dd9f71b315e61d871b38c79339d0cc3d26e100e1" + }, + { + "channel_id": "771570190275051521", + "chan_point": "6b2d5942bdadd04f26ad4f7ee86cceabd68531f66c7b370d27e02d9222c02bd1:1", + "last_update": 1710819115, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "750", + "disabled": true, + "max_htlc_msat": "990000000", + "last_update": 1710819115, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "200", + "fee_rate_milli_msat": "200", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1648881967, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403", + "node2_pub": "03d213f4115a55f0d0ab185434fcb6d3f119fdede11e901fe57f8b91d57ed316e6" + }, + { + "channel_id": "887983182865104897", + "chan_point": "713685d0215c28e1ea8f56357459afb65fc86fd26075d2cc760589ab82f2f053:1", + "last_update": 1710842515, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710842515, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710246269, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "031678745383bd273b4c3dbefc8ffbf4847d85c2f62d3407c0c980430b3257c403", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "756389233140760576", + "chan_point": "207b45f0cb6cb068b332bbdade71c936097d97277aafc212fea2548598398291:0", + "last_update": 1710393060, + "capacity": "250000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": true, + "max_htlc_msat": "247500000", + "last_update": 1710393060, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "247500000", + "last_update": 1662580373, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "024a65932bdad1c4ef070289a6dd9f71b315e61d871b38c79339d0cc3d26e100e1", + "node2_pub": "021227d4be948ab84613d5dbd47b6e148cfb811432d27eb92e80d28382b1b27d27" + }, + { + "channel_id": "918390177017757696", + "chan_point": "d46c48c0d9410ab9597860b8ba10c14ed4c1d86338f80a7f7ff36ecf881269b3:0", + "last_update": 1710806052, + "capacity": "16770000", + "node1_policy": { + "time_lock_delta": 72, + "min_htlc": "1000", + "fee_base_msat": "2147483647", + "fee_rate_milli_msat": "2147483647", + "disabled": false, + "max_htlc_msat": "15093000000", + "last_update": 1710804699, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "1677000000", + "last_update": 1710806052, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "024729f3d5f7794f4df09bfdb7ca23a4fbe6c38997cdd213c694a2cca1c27cbb17", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + }, + { + "channel_id": "899069558556065792", + "chan_point": "9483b700e1dfa3512defbb27097dc48da750ae6ea07b3baeb3c1a34da4cdec52:0", + "last_update": 1710846357, + "capacity": "40000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "10", + "disabled": true, + "max_htlc_msat": "39600000", + "last_update": 1710846357, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "39600000", + "last_update": 1700598649, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "025dfb3926e67a64b45a164865a737ecfc37071d11ac4846b0d59c4979c8bb08b8", + "node2_pub": "0321743efb2d44a9c1ff8194f03728e119bbf6cde88201b9ac8e6e48f96c99d597" + }, + { + "channel_id": "877726938508689409", + "chan_point": "5175773efb30bcae88dca5c47c6dcf8ba78b3fd8c31397d08db8f7b9e7e85019:1", + "last_update": 1710785354, + "capacity": "84803717", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "83955680000", + "last_update": 1710777182, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "83955680000", + "last_update": 1710785354, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + }, + { + "channel_id": "889215735335550977", + "chan_point": "6dafd3c899572ba7c9cb984fa9108c8a7eb1a93e46df25966fc17090f4a240c2:1", + "last_update": 1710776050, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710776050, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710772753, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + }, + { + "channel_id": "861583908627218433", + "chan_point": "21c74ee01ff0eda80631adcb10469619adecd70f3d937d27d4fec60f0225019d:1", + "last_update": 1710856349, + "capacity": "3000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "159", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710856349, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "2970000000", + "last_update": 1710795398, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + }, + { + "channel_id": "907088297038446592", + "chan_point": "6a5795e372ac56b4555dcc179a1f125d39f2f48800f079cd3ef8584cafae4817:0", + "last_update": 1710848353, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710774858, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 222, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710848353, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03d607f3e69fd032524a867b288216bfab263b6eaee4e07783799a6fe69bb84fac", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "737654654585667584", + "chan_point": "0bebc81ceefe54c2568e4b762f2878eb4e7a85514f9346ba4352034521447269:0", + "last_update": 1710812050, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 37, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1700", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710786335, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 140, + "min_htlc": "1000", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710812050, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "node2_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + }, + { + "channel_id": "894843035960147968", + "chan_point": "cfcfe8f1dc8cf0f20a6292facfc00d7e820e89cff3de64bd057801fc3ec3fc8d:0", + "last_update": 1710757535, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710757535, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710312917, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "884413068699238401", + "chan_point": "92dcfce24fc43ea96273efbe4304ea5a2f34c69ab8067d06bea37b3436926cad:1", + "last_update": 1710816935, + "capacity": "550000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "544500000", + "last_update": 1710816935, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "544500000", + "last_update": 1710801275, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "028d98b9969fbed53784a36617eb489a59ab6dc9b9d77fcdca9ff55307cd98e3c4", + "node2_pub": "03ea17fd97ca7382a192dd4533c8e95ca946ef0827b14984e682af699bcaeb5a53" + }, + { + "channel_id": "892859516987572224", + "chan_point": "726039ab50c25e38a2df12dc67394879c36bb8420dc481b843d0833c8ee93303:0", + "last_update": 1710835918, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710802736, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710835918, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "021cb32426ed1a6be9533801e7c56a70ffc1c360a3a31819c3ae0a72e141d78000", + "node2_pub": "0328946a0693c2a4dd7e5afc2ce2e020c54e07d6693a293b4e1c1fd7e2d09a3eeb" + }, + { + "channel_id": "907853557235646464", + "chan_point": "72d24dea72f3e930a219cfccfa352cc10040d1bcf49db365025d263e82c7de83:0", + "last_update": 1710776927, + "capacity": "50000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "50", + "disabled": false, + "max_htlc_msat": "49500000", + "last_update": 1710776927, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "49500000", + "last_update": 1710630223, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03919a0a495cfd08779a3c23168827243cabe597e8ee5ea0c7827b6c407e260fe2", + "node2_pub": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac" + }, + { + "channel_id": "763639412848394241", + "chan_point": "490f8ac59304fd951b06e6b56e07fba5623ccf567ed5ec0aa0f00f994a80995d:1", + "last_update": 1710846930, + "capacity": "100000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1709509568, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "99000000", + "last_update": 1710846930, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02626318f968469fb1dcd0453536bbabaab8861be75d8cde7900e57aab1bd4f3ac", + "node2_pub": "03b9de7a7d7e8a474b859a59e314cf2a143ae9c569ce78c46289b3ad16cb8c03f9" + }, + { + "channel_id": "855546490317897729", + "chan_point": "e1998ecf9d5bdf045a2b2e0fc868cb66532bc8e3c3910fac54039cb72e7b666b:1", + "last_update": 1710789782, + "capacity": "1100000", + "node1_policy": { + "time_lock_delta": 200, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "2000", + "disabled": false, + "max_htlc_msat": "1089000000", + "last_update": 1710789782, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1089000000", + "last_update": 1710777136, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3", + "node2_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f" + }, + { + "channel_id": "817245002822582272", + "chan_point": "4f774c8da0276be4aa4a488bff1877f1f0d42ce6d919b294d401ae9fceb5402b:0", + "last_update": 1710845182, + "capacity": "60000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "59400000", + "last_update": 1710845182, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "100", + "disabled": false, + "max_htlc_msat": "59400000", + "last_update": 1710775336, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "035fcbf3d34c71ffe7404c5660242f9289991021cc3d52098d0038f839552365a3", + "node2_pub": "035d681f3696fc5039d304c621f6f39ba6e0aa0ad482065417720d4f820f8dcb0e" + }, + { + "channel_id": "858923090495012865", + "chan_point": "6ecf4b8b50ffcaa4180c686d37ac2f7d5208a385b3ff6cdfacd7bd49b0795716:1", + "last_update": 1710780782, + "capacity": "650000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "643500000", + "last_update": 1710775476, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "1000", + "disabled": false, + "max_htlc_msat": "643500000", + "last_update": 1710780782, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f", + "node2_pub": "0209a06852f1257d70e5cea44a0919871fa20859eb33a62445f774e9fe96247a75" + }, + { + "channel_id": "837860845868220417", + "chan_point": "3188e2c87379e1784b904fc4ee2f6e75d846d87a9b8dd1db0e5217c11d8ce4ea:1", + "last_update": 1710775382, + "capacity": "3500000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "500", + "fee_rate_milli_msat": "1000", + "disabled": true, + "max_htlc_msat": "3465000000", + "last_update": 1710775382, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "3465000000", + "last_update": 1710537660, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f", + "node2_pub": "033f481fe0e9344228b58e0297162bfa8d648d5043c12b6323df5eac61bd39094c" + }, + { + "channel_id": "861833497883574273", + "chan_point": "0482621aeb0de1762640b99dfc600326903a52d11c8dcdf02a5e5d2652732c8f:1", + "last_update": 1710843782, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "4000", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710843782, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710773696, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "030c3f19d742ca294a55c00376b3b355c3c90d61c6b6b39554dbc7ac19b141c14f", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "840309458235949057", + "chan_point": "012e468c6298ff7a689d671bfca97387e45ab7a7b6e7fc3dbfa46e55a95c6b59:1", + "last_update": 1710825499, + "capacity": "2000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710825118, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "90", + "disabled": false, + "max_htlc_msat": "1980000000", + "last_update": 1710825499, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0", + "node2_pub": "0328946a0693c2a4dd7e5afc2ce2e020c54e07d6693a293b4e1c1fd7e2d09a3eeb" + }, + { + "channel_id": "826733788095447041", + "chan_point": "dffc24350df473a58d4715c31f8ed35a7440387dbf59e16895a01a79c3d64ce2:1", + "last_update": 1710805699, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "10000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "90", + "disabled": false, + "max_htlc_msat": "1000000000", + "last_update": 1710805699, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "800000000", + "last_update": 1710798104, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03440f4dd43f5e30ffa0fd37eb99e2c27241d71e4fc5b3ea1e9c04a289a51c7ae0", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "909819483881668608", + "chan_point": "735fca87374ceba0dff8adec0bffafb1a870638f79f2b44246f1686644d8cdc9:0", + "last_update": 1710783381, + "capacity": "200000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "30", + "disabled": true, + "max_htlc_msat": "198000000", + "last_update": 1710783323, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "100", + "disabled": true, + "max_htlc_msat": "198000000", + "last_update": 1710783381, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03eaf4f94ad680855a7818c2f156ae4a86482dea2f396320c336989ce5f49da880", + "node2_pub": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + }, + { + "channel_id": "900662750928764928", + "chan_point": "c5babe10624dc1f24fd3e20eb59d0e6dbfc7dedf1ea0eaccb70f55a7abf3b38c:0", + "last_update": 1710844661, + "capacity": "100000000", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710844661, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "999", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710009152, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "838462278610059264", + "chan_point": "6a7091a5715eda464ead722f2acbc4c5c1aeddab5fd4ae44c1ae9025b7f1d6d2:0", + "last_update": 1710846461, + "capacity": "50000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710846461, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "49500000000", + "last_update": 1710771291, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + }, + { + "channel_id": "846395255002038273", + "chan_point": "ee07f74fc67ad1534106327880fae377fe91c9c7776f9d46699d28f99e9684c5:1", + "last_update": 1710848050, + "capacity": "10000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710788861, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "9900000000", + "last_update": 1710848050, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02535215135eb832df0f9858ff775bd4ae0b8911c59e2828ff7d03b535b333e149", + "node2_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83" + }, + { + "channel_id": "898725411427385345", + "chan_point": "96fe24d1c0ae58e89df72f3a0898f2bacc0dd58d11938325a54c46ad7b728804:1", + "last_update": 1710856837, + "capacity": "1000000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "50000000", + "last_update": 1710856837, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "600", + "disabled": false, + "max_htlc_msat": "990000000", + "last_update": 1710832983, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02841286ba5314ac9a4a29eb8d558b30fec348f22e22351ff8896288dede68e6a7", + "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + }, + { + "channel_id": "910155934451761152", + "chan_point": "888c6225644dc0f6fa8d22807a3cf9acb99bc1558806e6b2d5f15dad6cacfb59:0", + "last_update": 1710834571, + "capacity": "250000", + "node1_policy": { + "time_lock_delta": 50, + "min_htlc": "1000", + "fee_base_msat": "100", + "fee_rate_milli_msat": "10", + "disabled": false, + "max_htlc_msat": "50000000", + "last_update": 1710782196, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "30", + "disabled": false, + "max_htlc_msat": "247500000", + "last_update": 1710834571, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "02841286ba5314ac9a4a29eb8d558b30fec348f22e22351ff8896288dede68e6a7", + "node2_pub": "03c7ec8ecec5386a6ce9476d8d55fa5bb22d113dce7c1c47c7df71b1741ffde972" + }, + { + "channel_id": "885818244503437313", + "chan_point": "0a0ac608d703bb383fd51e2ef0eebfb40e737d916f4e7810785ec8b9f7915d4e:1", + "last_update": 1710611588, + "capacity": "700000", + "node1_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "315000000", + "last_update": 1710558538, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": true, + "max_htlc_msat": "315000000", + "last_update": 1710611588, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "033f481fe0e9344228b58e0297162bfa8d648d5043c12b6323df5eac61bd39094c", + "node2_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f" + }, + { + "channel_id": "899068459022417920", + "chan_point": "f8751c293cfefc7e80bb4ae6c452ce17d33541ef420081b913ffc985ec246c16:0", + "last_update": 1710780553, + "capacity": "51000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "50490000", + "last_update": 1700598649, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1234", + "fee_rate_milli_msat": "1", + "disabled": true, + "max_htlc_msat": "50490000", + "last_update": 1710780553, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0321743efb2d44a9c1ff8194f03728e119bbf6cde88201b9ac8e6e48f96c99d597", + "node2_pub": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43" + }, + { + "channel_id": "846395255027269632", + "chan_point": "7fc299cd258401fef62f746434cefa05d42cd8895eec4f65d435ec2cd3bb3ec5:0", + "last_update": 1710848050, + "capacity": "20000000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710787491, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "0", + "disabled": false, + "max_htlc_msat": "19800000000", + "last_update": 1710848050, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "0335e4265f783f37378e969c6a123557cf5d22cc97ec42ea3abff5dfaa64afea83", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + }, + { + "channel_id": "917416009602236416", + "chan_point": "10f782b804347657cdb925866a7b9616f15dd8fd203b3de709245ecc808939a7:0", + "last_update": 1710785691, + "capacity": "99995000", + "node1_policy": { + "time_lock_delta": 140, + "min_htlc": "100", + "fee_base_msat": "2000", + "fee_rate_milli_msat": "2500", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710785691, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 144, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "499", + "disabled": false, + "max_htlc_msat": "20000000000", + "last_update": 1710265468, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f", + "node2_pub": "027cd974e47086291bb8a5b0160a889c738f2712a703b8ea939985fd16f3aae67e" + }, + { + "channel_id": "865735664596090880", + "chan_point": "317d529be3e15cccad634f20811a98c2ba686188b9cad9803a322e8b29c5e4bc:0", + "last_update": 1710845901, + "capacity": "75000", + "node1_policy": { + "time_lock_delta": 80, + "min_htlc": "1000", + "fee_base_msat": "0", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "74250000", + "last_update": 1710845901, + "custom_records": {} + }, + "node2_policy": { + "time_lock_delta": 40, + "min_htlc": "1000", + "fee_base_msat": "1000", + "fee_rate_milli_msat": "1", + "disabled": false, + "max_htlc_msat": "74250000", + "last_update": 1710845871, + "custom_records": {} + }, + "custom_records": {}, + "node1_pub": "039157862ec407b1aa2353d879279916e76c8d2fcc4ac10d3f225972c8e58fbc43", + "node2_pub": "03382704812aca55bf853b353ecd3edef7e16a9420d7beb7e7d6b6c7fe2082252a" + } + ] +} \ No newline at end of file diff --git a/test/data/admin/namespaces/two_namespaces_two_users/namespace-defaults.yaml b/test/data/admin/namespaces/two_namespaces_two_users/namespace-defaults.yaml new file mode 100644 index 000000000..75cc8e42c --- /dev/null +++ b/test/data/admin/namespaces/two_namespaces_two_users/namespace-defaults.yaml @@ -0,0 +1,18 @@ +users: + - name: warnet-user + roles: + - pod-viewer + - pod-manager +# the pod-viewer and pod-manager roles are the default +# roles defined in values.yaml for the namespaces charts +# +# if you need a different set of roles for a particular namespaces +# deployment, you can override values.yaml by providing your own +# role definitions below +# +# roles: +# - name: my-custom-role +# rules: +# - apiGroups: "" +# resources: "" +# verbs: "" diff --git a/test/data/admin/namespaces/two_namespaces_two_users/namespaces.yaml b/test/data/admin/namespaces/two_namespaces_two_users/namespaces.yaml new file mode 100644 index 000000000..413d3bcb7 --- /dev/null +++ b/test/data/admin/namespaces/two_namespaces_two_users/namespaces.yaml @@ -0,0 +1,19 @@ +namespaces: + - name: wargames-red-team-warnettest + users: + - name: alice-warnettest + roles: + - pod-viewer + - name: bob-warnettest + roles: + - pod-viewer + - pod-manager + - name: wargames-blue-team-warnettest + users: + - name: mallory-warnettest + roles: + - pod-viewer + - name: carol-warnettest + roles: + - pod-viewer + - pod-manager diff --git a/test/data/bitcoin_conf/network.yaml b/test/data/bitcoin_conf/network.yaml new file mode 100644 index 000000000..06ec79290 --- /dev/null +++ b/test/data/bitcoin_conf/network.yaml @@ -0,0 +1,64 @@ +nodes: + - name: tank-0016 + image: + tag: "0.16.1" + addnode: + - tank-0017 + config: + uacomment=tank-0016 + - name: tank-0017 + image: + tag: "0.17.0" + addnode: + - tank-0019 + config: + uacomment=tank-0017 + - name: tank-0019 + image: + tag: "0.19.2" + addnode: + - tank-0020 + config: + uacomment=tank-0019 + - name: tank-0020 + image: + tag: "0.20.0" + addnode: + - tank-0021 + config: + uacomment=tank-0020 + - name: tank-0021 + image: + tag: "0.21.1" + addnode: + - tank-0024 + config: + uacomment=tank-0021 + - name: tank-0024 + image: + tag: "24.2" + addnode: + - tank-0025 + config: + uacomment=tank-0024 + - name: tank-0025 + image: + tag: "25.1" + addnode: + - tank-0026 + config: + uacomment=tank-0025 + - name: tank-0026 + image: + tag: "26.0" + addnode: + - tank-0027 + config: + uacomment=tank-0026 + - name: tank-0027 + image: + tag: "27.0" + addnode: + - tank-0016 + config: + uacomment=tank-0027 \ No newline at end of file diff --git a/test/data/bitcoin_conf/node-defaults.yaml b/test/data/bitcoin_conf/node-defaults.yaml new file mode 100644 index 000000000..a7ca1a373 --- /dev/null +++ b/test/data/bitcoin_conf/node-defaults.yaml @@ -0,0 +1,12 @@ +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" + +resources: + limits: + cpu: 4000m + memory: 500Mi + requests: + cpu: 100m + memory: 200Mi \ No newline at end of file diff --git a/test/data/build_v24_test.graphml b/test/data/build_v24_test.graphml deleted file mode 100644 index 5dc8c7297..000000000 --- a/test/data/build_v24_test.graphml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - 27.0 - -uacomment=w0 - - - bitcoin/bitcoin#24.x - -uacomment=v24_build - --disable-zmq - - - - diff --git a/test/data/ln.graphml b/test/data/ln.graphml deleted file mode 100644 index e0606c93f..000000000 --- a/test/data/ln.graphml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - simln - - 27.0 - -uacomment=w0 - lnd - lightninglabs/lnd:v0.17.5-beta - true - - - 27.0 - -uacomment=w1 - lnd - pinheadmz/circuitbreaker:278737d - true - - - 27.0 - -uacomment=w2 - lnd - pinheadmz/circuitbreaker:278737d - --bitcoin.timelockdelta=33 - - - 27.0 - -uacomment=w2 - cln - --cltv-delta=33 - - - 27.0 - -uacomment=w3 - - - - - - - - - - --local_amt=100000 - --base_fee_msat=2200 --fee_rate_ppm=13 --time_lock_delta=20 - - - --local_amt=100000 --push_amt=50000 - --base_fee_msat=5500 --fee_rate_ppm=3 --time_lock_delta=40 - - - amount=100000 push_msat=50000000 - feebase=5500 feeppm=3 - - - \ No newline at end of file diff --git a/test/data/ln/network.yaml b/test/data/ln/network.yaml new file mode 100644 index 000000000..a5e10ad7c --- /dev/null +++ b/test/data/ln/network.yaml @@ -0,0 +1,53 @@ +nodes: + - name: tank-0000 + addnode: + - tank-0001 + ln: + lnd: false + cln: true + - name: tank-0001 + addnode: + - tank-0002 + - name: tank-0002 + addnode: + - tank-0000 + - name: tank-0003 + addnode: + - tank-0000 + lnd: + config: | + bitcoin.timelockdelta=33 + channels: + - id: + block: 500 + index: 1 + target: tank-0004-ln + capacity: 100000 + push_amt: 50000 + circuitbreaker: + enabled: true + httpPort: 9235 + # Just 32 bytes of entropy encoded in base64 + macaroonRootKey: nmPScpcYkBBUXvEryzpYfjgY27j8hO9SiXO9qNQAJFs= + # Derived from root key with `lncli bakemacaroon --root_key=...` + adminMacaroon: 0201036c6e6402f801030a10b676fd9c5cbdfccedb1b932865ad57451201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620cff489a728f1be62c41db20607e78e3b3173f5b51679b4489e429cc83648dac4 + + - name: tank-0004 + addnode: + - tank-0000 + lnd: + channels: + - id: + block: 500 + index: 2 + target: tank-0005-ln + capacity: 50000 + push_amt: 25000 + # Just 32 bytes of entropy encoded in base64 + macaroonRootKey: FmEMD2X1hKzxR5yAWgbAT5CbQWPOW+OdyztMMCTBThU= + # Derived from root key with `lncli bakemacaroon --root_key=...` + adminMacaroon: 0201036c6e6402f801030a102616e41d07f5df28869fe8f8147ed2861201301a160a0761646472657373120472656164120577726974651a130a04696e666f120472656164120577726974651a170a08696e766f69636573120472656164120577726974651a210a086d616361726f6f6e120867656e6572617465120472656164120577726974651a160a076d657373616765120472656164120577726974651a170a086f6666636861696e120472656164120577726974651a160a076f6e636861696e120472656164120577726974651a140a057065657273120472656164120577726974651a180a067369676e6572120867656e657261746512047265616400000620e40032ba0d91a7f046ed4596da67aa129cca0ff162e51444df2ce68fa6d16944 + + - name: tank-0005 + addnode: + - tank-0000 \ No newline at end of file diff --git a/test/data/ln/node-defaults.yaml b/test/data/ln/node-defaults.yaml new file mode 100644 index 000000000..be5b8bcd3 --- /dev/null +++ b/test/data/ln/node-defaults.yaml @@ -0,0 +1,40 @@ +# enable collectLogs and metricsExport to activate publish lnd-exporter metrics + +#Core configs +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" +collectLogs: false +metricsExport: false + +#LN configs +ln: + lnd: true +lnd: + defaultConfig: | + color=#000000 + config: | + bitcoin.timelockdelta=33 + metricsExport: false + prometheusMetricsPort: 9332 + extraContainers: + - name: lnd-exporter + image: bitcoindevproject/lnd-exporter:0.2.0 + imagePullPolicy: IfNotPresent + volumeMounts: + - name: config + mountPath: /macaroon.hex + subPath: MACAROON_HEX + env: + - name: METRICS + value: > + lnd_balance_channels=parse("/v1/balance/channels","balance") + lnd_local_balance_channels=parse("/v1/balance/channels","local_balance.sat") + lnd_remote_balance_channels=parse("/v1/balance/channels","remote_balance.sat") + lnd_block_height=parse("/v1/getinfo","block_height") + lnd_peers=parse("/v1/getinfo","num_peers") + ports: + - name: prom-metrics + containerPort: 9332 + protocol: TCP \ No newline at end of file diff --git a/test/data/logging.graphml b/test/data/logging.graphml deleted file mode 100644 index 54b3b73cb..000000000 --- a/test/data/logging.graphml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - 27.0 - true - - - 27.0 - true - txrate=getchaintxstats(10)["txrate"] - - - 27.0 - - - - - - diff --git a/test/data/logging/network.yaml b/test/data/logging/network.yaml new file mode 100644 index 000000000..129de21a1 --- /dev/null +++ b/test/data/logging/network.yaml @@ -0,0 +1,37 @@ +nodes: + - name: tank-0000 + addnode: + - tank-0002 + metricsExport: true + - name: tank-0001 + addnode: + - tank-0002 + metricsExport: true + metrics: txrate=getchaintxstats(10)["txrate"] ipv4peers=COUNT:getpeerinfo(),network,ipv4 + - name: tank-0002 + addnode: + - tank-0000 + ln: + lnd: true + lnd: + metricsExport: true + prometheusMetricsPort: 9332 + extraContainers: + - name: lnd-exporter + image: bitcoindevproject/lnd-exporter:0.2.0 + imagePullPolicy: IfNotPresent + volumeMounts: + - name: config + mountPath: /macaroon.hex + subPath: MACAROON_HEX + env: + - name: METRICS + value: > + lnd_block_height=parse("/v1/getinfo","block_height") + lnd_peers=parse("/v1/getinfo","num_peers") + ports: + - name: prom-metrics + containerPort: 9332 + protocol: TCP +caddy: + enabled: true \ No newline at end of file diff --git a/test/data/logging/node-defaults.yaml b/test/data/logging/node-defaults.yaml new file mode 100644 index 000000000..b914c8bba --- /dev/null +++ b/test/data/logging/node-defaults.yaml @@ -0,0 +1,5 @@ +collectLogs: true +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" diff --git a/test/data/network_with_plugins/network.yaml b/test/data/network_with_plugins/network.yaml new file mode 100644 index 000000000..c0d8d9c7f --- /dev/null +++ b/test/data/network_with_plugins/network.yaml @@ -0,0 +1,91 @@ +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: + config: | + bitcoin.timelockdelta=33 + channels: + - id: + block: 500 + index: 1 + target: tank-0004-ln + capacity: 100000 + push_amt: 50000 + circuitbreaker: + enabled: true # This enables circuitbreaker for this node + httpPort: 9235 # Can override defaults per-node + + - name: tank-0004 + addnode: + - tank-0000 + ln: + lnd: true + lnd: + channels: + - id: + block: 500 + index: 2 + target: tank-0005-ln + capacity: 50000 + push_amt: 25000 + + - name: tank-0005 + addnode: + - tank-0000 + ln: + lnd: true + +plugins: # Each plugin section has a number of hooks available (preDeploy, postDeploy, etc) + preDeploy: # For example, the preDeploy hook means it's plugin will run before all other deploy code + hello: + entrypoint: "../plugins/hello" # This entrypoint path is relative to the network.yaml file + podName: "hello-pre-deploy" + helloTo: "preDeploy!" + postDeploy: + hello: + entrypoint: "../plugins/hello" + podName: "hello-post-deploy" + helloTo: "postDeploy!" + simln: # You can have multiple plugins per hook + entrypoint: "../../../resources/plugins/simln" + activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' + preNode: # preNode plugins run before each node is deployed + hello: + entrypoint: "../plugins/hello" + helloTo: "preNode!" + postNode: + hello: + entrypoint: "../plugins/hello" + helloTo: "postNode!" + preNetwork: + hello: + entrypoint: "../plugins/hello" + helloTo: "preNetwork!" + podName: "hello-pre-network" + postNetwork: + hello: + entrypoint: "../plugins/hello" + helloTo: "postNetwork!" + podName: "hello-post-network" + \ No newline at end of file diff --git a/test/data/network_with_plugins/node-defaults.yaml b/test/data/network_with_plugins/node-defaults.yaml new file mode 100644 index 000000000..24a00b5c8 --- /dev/null +++ b/test/data/network_with_plugins/node-defaults.yaml @@ -0,0 +1,8 @@ +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" + +lnd: + defaultConfig: | + color=#000000 diff --git a/test/data/onion/network.yaml b/test/data/onion/network.yaml new file mode 100644 index 000000000..be4aa77dc --- /dev/null +++ b/test/data/onion/network.yaml @@ -0,0 +1,23 @@ +caddy: + enabled: false +fork_observer: + configQueryInterval: 20 + enabled: false +nodes: +- image: + tag: '29.0' + name: tank-0000 +- image: + tag: '29.0' + name: tank-0001 +- image: + tag: '29.0' + name: tank-0002 +- image: + tag: '29.0' + name: tank-0003 + +plugins: + preDeploy: + tor: + entrypoint: "../../../resources/plugins/tor" \ No newline at end of file diff --git a/test/data/onion/node-defaults.yaml b/test/data/onion/node-defaults.yaml new file mode 100644 index 000000000..9038774c6 --- /dev/null +++ b/test/data/onion/node-defaults.yaml @@ -0,0 +1,22 @@ +chain: regtest +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent +defaultConfig: | + debug=net + debug=tor + proxy=127.0.0.1:9050 + listen=1 + onlynet=onion + torcontrol=127.0.0.1:9051 + +collectLogs: false +metricsExport: false + +extraContainers: + - name: tor + image: bitcoindevproject/tor-relay:latest + ports: + - name: toror + containerPort: 9001 + protocol: TCP \ No newline at end of file diff --git a/test/data/permutations.graphml b/test/data/permutations.graphml deleted file mode 100644 index 0c4686f61..000000000 --- a/test/data/permutations.graphml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - 27.0 - - - - False - False - - - bitcoindevproject/bitcoin:26.0 - - - - False - False - - - 27.0 - - - - False - False - - - 27.0 - - - - False - False - - - 27.0 - - - - False - False - - - 27.0 - - - - False - False - - - 27.0 - - - - False - False - - - 27.0 - - - - False - False - - - - - - - - - - - - \ No newline at end of file diff --git a/test/data/plugins/hello/README.md b/test/data/plugins/hello/README.md new file mode 100644 index 000000000..77bb5040f --- /dev/null +++ b/test/data/plugins/hello/README.md @@ -0,0 +1,124 @@ +# Hello Plugin + +## Hello World! +*Hello* is an example plugin to demonstrate the features of Warnet's plugin architecture. It uses each of the hooks available in the `warnet deploy` command (see the example below for details). + +## Usage +In your python virtual environment with Warnet installed and setup, create a new Warnet user folder (follow the prompts): + +`$ warnet new user_folder` + +`$ cd user_folder` + +Deploy the *hello* network. + +`$ warnet deploy networks/hello` + +While that is launching, take a look inside the `networks/hello/network.yaml` file. You can also see the copy below which includes commentary on the structure of plugins in the `network.yaml` file. + +Also, take a look at the `plugins/hello/plugin.py` file to see how plugins work and to find out how to author your own plugin. + +Once `deploy` completes, view the pods of the *hello* network by invoking `kubectl get all -A`. + +To view the various "Hello World!" messages, run `kubectl logs pod/POD_NAME` + +### A `network.yaml` example +When you initialize a new Warnet network, Warnet will create a new `network.yaml` file. You can modify these files to fit your needs. + +For example, the `network.yaml` file below includes the *hello* plugin, lightning nodes, and the *simln* plugin. + +
+network.yaml + +````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: + config: | + bitcoin.timelockdelta=33 + channels: + - id: + block: 300 + index: 1 + target: tank-0004-ln + capacity: 100000 + push_amt: 50000 + + - name: tank-0004 + addnode: + - tank-0000 + ln: + lnd: true + lnd: + channels: + - id: + block: 300 + index: 2 + target: tank-0005-ln + capacity: 50000 + push_amt: 25000 + + - name: tank-0005 + addnode: + - tank-0000 + ln: + lnd: true + +plugins: # Each plugin section has a number of hooks available (preDeploy, postDeploy, etc) + preDeploy: # For example, the preDeploy hook means it's plugin will run before all other deploy code + hello: + entrypoint: "../../plugins/hello" # This entrypoint path is relative to the network.yaml file + podName: "hello-pre-deploy" + helloTo: "preDeploy!" + postDeploy: + hello: + entrypoint: "../../plugins/hello" + podName: "hello-post-deploy" + helloTo: "postDeploy!" + simln: # You can have multiple plugins per hook + entrypoint: "../../plugins/simln" + activity: '[{"source": "tank-0003-ln", "destination": "tank-0005-ln", "interval_secs": 1, "amount_msat": 2000}]' + preNode: # preNode plugins run before each node is deployed + hello: + entrypoint: "../../plugins/hello" + helloTo: "preNode!" + postNode: + hello: + entrypoint: "../../plugins/hello" + helloTo: "postNode!" + preNetwork: + hello: + entrypoint: "../../plugins/hello" + helloTo: "preNetwork!" + podName: "hello-pre-network" + postNetwork: + hello: + entrypoint: "../../plugins/hello" + helloTo: "postNetwork!" + podName: "hello-post-network" +```` + +
+ diff --git a/test/data/plugins/hello/charts/hello/Chart.yaml b/test/data/plugins/hello/charts/hello/Chart.yaml new file mode 100644 index 000000000..abd94467e --- /dev/null +++ b/test/data/plugins/hello/charts/hello/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +name: hello-chart +description: A Helm chart for a hello Pod +version: 0.1.0 +appVersion: "1.0" \ No newline at end of file diff --git a/test/data/plugins/hello/charts/hello/templates/pod.yaml b/test/data/plugins/hello/charts/hello/templates/pod.yaml new file mode 100644 index 000000000..ba5319670 --- /dev/null +++ b/test/data/plugins/hello/charts/hello/templates/pod.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ .Values.podName }} + labels: + app: {{ .Chart.Name }} +spec: + restartPolicy: Never + containers: + - name: {{ .Values.podName }}-container + image: alpine:latest + command: ["sh", "-c"] + args: + - echo "Hello {{ .Values.helloTo }}"; + resources: {} \ No newline at end of file diff --git a/test/data/plugins/hello/charts/hello/values.yaml b/test/data/plugins/hello/charts/hello/values.yaml new file mode 100644 index 000000000..302da3c15 --- /dev/null +++ b/test/data/plugins/hello/charts/hello/values.yaml @@ -0,0 +1,2 @@ +podName: hello-pod +helloTo: "world" \ No newline at end of file diff --git a/test/data/plugins/hello/plugin.py b/test/data/plugins/hello/plugin.py new file mode 100755 index 000000000..cb5a8373a --- /dev/null +++ b/test/data/plugins/hello/plugin.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +import json +import logging +from enum import Enum +from pathlib import Path +from typing import Optional + +import click + +from warnet.constants import PLUGIN_ANNEX, AnnexMember, HookValue, WarnetContent +from warnet.process import run_command + +# It is common for Warnet objects to have a "mission" label to help query them in the cluster. +MISSION = "hello" +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 + + +# Plugins look like this in the `network.yaml` file: +# +# plugins: +# hello: +# podName: "a-pod-name" +# helloTo: "World!" +# +# "podName" and "helloTo" are essentially dictionary keys, and it helps to keep those keys in an +# enum in order to prevent typos. +class PluginContent(Enum): + POD_NAME = "podName" + HELLO_TO = "helloTo" + + +# Warnet uses a python package called "click" to manage terminal interactions with the user. +# To use click, we must declare a click "group" by decorating a function named after the plugin. +# While optional, using click makes it easy for users to interact with your plugin. +@click.group() +@click.pass_context +def hello(ctx): + """Commands for the Hello plugin""" + ctx.ensure_object(dict) + plugin_dir = Path(__file__).resolve().parent + ctx.obj[PLUGIN_DIR_TAG] = Path(plugin_dir) + + +# Each Warnet plugin must have an entrypoint function which takes two JSON objects: plugin_content +# and warnet_content. We have seen the PluginContent enum above. Warnet also has a WarnetContent +# enum which holds the keys to the warnet_content dictionary. +@hello.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] + + if hook_value in ( + HookValue.PRE_NETWORK, + HookValue.POST_NETWORK, + HookValue.PRE_DEPLOY, + HookValue.POST_DEPLOY, + ): + data = get_data(plugin_content) + if data: + _launch_pod(ctx, install_name=hook_value.value.lower() + "-hello", **data) + else: + _launch_pod(ctx, install_name=hook_value.value.lower() + "-hello") + elif hook_value == HookValue.PRE_NODE: + name = warnet_content[PLUGIN_ANNEX][AnnexMember.NODE_NAME.value] + "-pre-hello-pod" + _launch_pod(ctx, install_name=hook_value.value.lower() + "-" + name, podName=name) + elif hook_value == HookValue.POST_NODE: + name = warnet_content[PLUGIN_ANNEX][AnnexMember.NODE_NAME.value] + "-post-hello-pod" + _launch_pod(ctx, install_name=hook_value.value.lower() + "-" + name, podName=name) + + +def get_data(plugin_content: dict) -> Optional[dict]: + data = { + key: plugin_content.get(key) + for key in (PluginContent.POD_NAME.value, PluginContent.HELLO_TO.value) + if plugin_content.get(key) + } + return data or None + + +def _launch_pod( + ctx, install_name: str = "hello", podName: str = "hello-pod", helloTo: str = "World!" +): + command = ( + f"helm upgrade --install {install_name} {ctx.obj[PLUGIN_DIR_TAG]}/charts/hello " + f"--set podName={podName} --set helloTo={helloTo}" + ) + log.info(command) + log.info(run_command(command)) + + +if __name__ == "__main__": + hello() diff --git a/test/data/services.graphml b/test/data/services.graphml deleted file mode 100644 index c9e0a0d01..000000000 --- a/test/data/services.graphml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - 27.0 - -uacomment=w0 -debug=validation - true - lnd - - - diff --git a/test/data/services/network.yaml b/test/data/services/network.yaml new file mode 100644 index 000000000..d8709f857 --- /dev/null +++ b/test/data/services/network.yaml @@ -0,0 +1,42 @@ +nodes: + - name: john + config: | + dns=1 + debug=rpc + rpcauth=forkobserver:1418183465eecbd407010cf60811c6a0$d4e5f0647a63429c218da1302d7f19fe627302aeb0a71a74de55346a25d8057c + rpcwhitelist=forkobserver:getchaintips,getblockheader,getblockhash,getblock,getnetworkinfo + rpcwhitelistdefault=0 + addnode: + - paul + - name: paul + config: | + dns=1 + debug=rpc + rpcauth=forkobserver:1418183465eecbd407010cf60811c6a0$d4e5f0647a63429c218da1302d7f19fe627302aeb0a71a74de55346a25d8057c + rpcwhitelist=forkobserver:getchaintips,getblockheader,getblockhash,getblock,getnetworkinfo + rpcwhitelistdefault=0 + - name: george + config: | + dns=1 + debug=rpc + rpcauth=forkobserver:1418183465eecbd407010cf60811c6a0$d4e5f0647a63429c218da1302d7f19fe627302aeb0a71a74de55346a25d8057c + rpcwhitelist=forkobserver:getchaintips,getblockheader,getblockhash,getblock,getnetworkinfo + rpcwhitelistdefault=0 + addnode: + - ringo + - name: ringo + config: | + dns=1 + debug=rpc + rpcauth=forkobserver:1418183465eecbd407010cf60811c6a0$d4e5f0647a63429c218da1302d7f19fe627302aeb0a71a74de55346a25d8057c + rpcwhitelist=forkobserver:getchaintips,getblockheader,getblockhash,getblock,getnetworkinfo + rpcwhitelistdefault=0 +fork_observer: + enabled: true +caddy: + enabled: true +services: + - title: Ringo REST + path: /ringo/ + host: ringo.default + port: 18443 \ No newline at end of file diff --git a/test/data/services/node-defaults.yaml b/test/data/services/node-defaults.yaml new file mode 100644 index 000000000..7e021cad1 --- /dev/null +++ b/test/data/services/node-defaults.yaml @@ -0,0 +1,4 @@ +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" diff --git a/test/data/signet-signer.json b/test/data/signet-signer.json new file mode 100644 index 000000000..6cf25b9fd --- /dev/null +++ b/test/data/signet-signer.json @@ -0,0 +1,52 @@ +{ + "address": + { + "address": "tb1q6vakuyw2jhzwmnxcaxryxs6c670fr9esfvrrhj", + "scriptPubKey": "0014d33b6e11ca95c4edccd8e986434358d79e919730", + "ismine": true, + "solvable": true, + "desc": "wpkh([2e239023/84h/1h/0h/0/0]03d28a77a6ea884727049c72818c312d1f7fefb4aa5e62c9211403fd6218eaa6fa)#vp55uze6", + "parent_desc": "wpkh([2e239023/84h/1h/0h]tpubDDsmgRhtuNzhYbEiUbucryCNyjshjKf4fawmVWAAwej5HhSLmXVWD9z8U8QHgaSQmYmGBfTfab6nsM4bLQkRR1qdpazc258PGtcyVJeDLbj/0/*)#r5lwvzmj", + "iswatchonly": false, + "isscript": false, + "iswitness": true, + "witness_version": 0, + "witness_program": "d33b6e11ca95c4edccd8e986434358d79e919730", + "pubkey": "03d28a77a6ea884727049c72818c312d1f7fefb4aa5e62c9211403fd6218eaa6fa", + "ischange": false, + "timestamp": 1725633470, + "hdkeypath": "m/84h/1h/0h/0/0", + "hdseedid": "0000000000000000000000000000000000000000", + "hdmasterfingerprint": "2e239023", + "labels": [ + "bech32" + ] + }, + "descriptors": + [ + { + "desc": "wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/0/*)#5j6mshps", + "timestamp": 0, + "active": true, + "internal": false, + "range": [ + 0, + 999 + ], + "next": 0, + "next_index": 0 + }, + { + "desc": "wpkh(tprv8ZgxMBicQKsPfH87iaMtrpzTkWiyFDW7SVWqfsKAhtyEBEqMV6ctPdtc5pNrb2FpSmPcDe8NrxEouUnWj1ud7LT1X1hB1XHKAgB2Z5Z4u2s/84h/1h/0h/1/*)#9xl6dz3g", + "timestamp": 0, + "active": true, + "internal": true, + "range": [ + 0, + 999 + ], + "next": 0, + "next_index": 0 + } + ] +} \ No newline at end of file diff --git a/test/data/signet/network.yaml b/test/data/signet/network.yaml new file mode 100644 index 000000000..630f6f626 --- /dev/null +++ b/test/data/signet/network.yaml @@ -0,0 +1,84 @@ +nodes: + - name: miner + image: + tag: "29.0-util" + - name: tank-1 + image: + tag: "0.16.1" + addnode: + - miner + - name: tank-2 + image: + tag: "0.17.0" + addnode: + - miner + - name: tank-3 + image: + tag: "0.19.2" + addnode: + - miner + - name: tank-4 + image: + tag: "0.20.0" + addnode: + - miner + - name: tank-5 + image: + tag: "0.21.1" + addnode: + - miner + - name: tank-6 + image: + tag: "24.2" + addnode: + - miner + - name: tank-7 + image: + tag: "25.1" + addnode: + - miner + - name: tank-8 + image: + tag: "26.0" + addnode: + - miner + - name: tank-9 + image: + tag: "27.0" + addnode: + - miner + - name: tank-10 + image: + tag: "0.16.1" + addnode: + - miner + - name: tank-11 + image: + tag: "98.0.0-invalid-blocks" + addnode: + - miner + - name: tank-12 + image: + tag: "94.0.0-5k-inv" + addnode: + - miner + - name: tank-13 + image: + tag: "95.0.0-disabled-opcodes" + addnode: + - miner + - name: tank-14 + image: + tag: "96.0.0-no-mp-trim" + addnode: + - miner + - name: tank-15 + image: + tag: "97.0.0-50-orphans" + addnode: + - miner + - name: tank-16 + image: + tag: "99.0.0-unknown-message" + addnode: + - miner diff --git a/test/data/signet/node-defaults.yaml b/test/data/signet/node-defaults.yaml new file mode 100644 index 000000000..941f03881 --- /dev/null +++ b/test/data/signet/node-defaults.yaml @@ -0,0 +1,15 @@ +image: + repository: bitcoindevproject/bitcoin + pullPolicy: Always + tag: "27.0" + +global: + chain: signet + +spec: + restartPolicy: Never + +defaultConfig: | + debug=rpc + debug=net + signetchallenge=0014d33b6e11ca95c4edccd8e986434358d79e919730 \ No newline at end of file diff --git a/test/data/ten_semi_unconnected.graphml b/test/data/ten_semi_unconnected.graphml deleted file mode 100644 index c2277407c..000000000 --- a/test/data/ten_semi_unconnected.graphml +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - 26.0 - - - - False - False - - - bitcoindevproject/bitcoin:26.0 - - - - False - False - - - 26.0 - - - - False - False - - - 26.0 - - - - False - False - - - 26.0 - - - - False - False - - - 26.0 - - - - False - False - - - 26.0 - - - - False - False - - - 26.0 - - - - False - False - - - 26.0 - - - - False - False - - - 26.0 - - - - False - False - - - - diff --git a/test/data/ten_semi_unconnected/network.yaml b/test/data/ten_semi_unconnected/network.yaml new file mode 100644 index 000000000..f058ac70f --- /dev/null +++ b/test/data/ten_semi_unconnected/network.yaml @@ -0,0 +1,31 @@ +nodes: + - name: tank-0000 + config: | + debug=rpc + debug=validation + - name: tank-0001 + config: | + debug=net + debug=validation + - name: tank-0002 + config: | + debug=validation + - name: tank-0003 + config: | + debug=validation + - name: tank-0004 + - name: tank-0005 + config: | + debug=validation + - name: tank-0006 + - name: tank-0007 + config: | + debug=validation + - name: tank-0008 + addnode: + - tank-0009 + config: | + debug=validation + - name: tank-0009 + config: | + debug=validation diff --git a/test/data/ten_semi_unconnected/node-defaults.yaml b/test/data/ten_semi_unconnected/node-defaults.yaml new file mode 100644 index 000000000..7e021cad1 --- /dev/null +++ b/test/data/ten_semi_unconnected/node-defaults.yaml @@ -0,0 +1,4 @@ +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: "27.0" diff --git a/test/data/wargames/namespaces/armies/namespace-defaults.yaml b/test/data/wargames/namespaces/armies/namespace-defaults.yaml new file mode 100644 index 000000000..fa7f68ecc --- /dev/null +++ b/test/data/wargames/namespaces/armies/namespace-defaults.yaml @@ -0,0 +1,6 @@ +users: + - name: warnet-user + roles: + - pod-viewer + - pod-manager +podLimit: 3 diff --git a/test/data/wargames/namespaces/armies/namespaces.yaml b/test/data/wargames/namespaces/armies/namespaces.yaml new file mode 100644 index 000000000..86cde68af --- /dev/null +++ b/test/data/wargames/namespaces/armies/namespaces.yaml @@ -0,0 +1,2 @@ +namespaces: +- name: wargames-red \ No newline at end of file diff --git a/test/data/wargames/networks/armada/network.yaml b/test/data/wargames/networks/armada/network.yaml new file mode 100644 index 000000000..9cb614810 --- /dev/null +++ b/test/data/wargames/networks/armada/network.yaml @@ -0,0 +1,6 @@ +nodes: +- name: armada + image: + tag: '27.0' + addnode: + - miner.default \ No newline at end of file diff --git a/test/data/wargames/networks/armada/node-defaults.yaml b/test/data/wargames/networks/armada/node-defaults.yaml new file mode 100644 index 000000000..bb219cf19 --- /dev/null +++ b/test/data/wargames/networks/armada/node-defaults.yaml @@ -0,0 +1,6 @@ +global: + chain: regtest +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: '27.0' \ No newline at end of file diff --git a/test/data/wargames/networks/battlefield/network.yaml b/test/data/wargames/networks/battlefield/network.yaml new file mode 100644 index 000000000..5cf96504f --- /dev/null +++ b/test/data/wargames/networks/battlefield/network.yaml @@ -0,0 +1,10 @@ +nodes: +- name: miner + image: + tag: '27.0' +- name: target-red + addnode: + - miner + image: + tag: '27.0' + \ No newline at end of file diff --git a/test/data/wargames/networks/battlefield/node-defaults.yaml b/test/data/wargames/networks/battlefield/node-defaults.yaml new file mode 100644 index 000000000..7399f5c34 --- /dev/null +++ b/test/data/wargames/networks/battlefield/node-defaults.yaml @@ -0,0 +1,6 @@ +global: + chain: regtest +image: + repository: bitcoindevproject/bitcoin + pullPolicy: IfNotPresent + tag: '27.0' diff --git a/test/graph_test.py b/test/graph_test.py index 68485f93d..60888180d 100755 --- a/test/graph_test.py +++ b/test/graph_test.py @@ -2,111 +2,89 @@ import json import os -import tempfile -import uuid -from pathlib import Path +import sys +import pexpect from test_base import TestBase -from warnet.lnd import LNDNode -from warnet.utils import DEFAULT_TAG + +from warnet.process import stream_command + +NETWORKS_DIR = "networks" class GraphTest(TestBase): def __init__(self): super().__init__() - self.graph_file_path = Path(os.path.dirname(__file__)) / "data" / "services.graphml" - self.json_file_path = Path(os.path.dirname(__file__)) / "data" / "LN_10.json" - self.NUM_IMPORTED_NODES = 10 - self.test_dir = tempfile.TemporaryDirectory() - self.tf_create = f"{self.test_dir.name}/{str(uuid.uuid4())}.graphml" - self.tf_import = f"{self.test_dir.name}/{str(uuid.uuid4())}.graphml" def run_test(self): - self.test_graph_creation_and_import() - self.validate_graph_schema() - - self.start_server() try: - self.test_graph_with_optional_services() - self.test_created_graph() - self.test_imported_graph() + # cwd out of the git repo for remainder of script + os.chdir(self.tmpdir) + self.directory_not_exist() + os.mkdir(NETWORKS_DIR) + self.directory_exists() + self.run_created_network() finally: - self.stop_server() - - def test_graph_creation_and_import(self): - self.log.info(f"CLI tool creating test graph file: {self.tf_create}") - self.log.info( - self.warcli( - f"graph create 10 --outfile={self.tf_create} --version={DEFAULT_TAG}", network=False - ) - ) - self.wait_for_predicate(lambda: Path(self.tf_create).exists()) - - self.log.info(f"CLI tool importing json and writing test graph file: {self.tf_import}") - self.log.info( - self.warcli( - f"graph import-json {self.json_file_path} --outfile={self.tf_import} --ln_image=carlakirkcohen/lnd:attackathon --cb=carlakirkcohen/circuitbreaker:attackathon-test", - network=False, - ) - ) - self.wait_for_predicate(lambda: Path(self.tf_import).exists()) - - def validate_graph_schema(self): - self.log.info("Validating graph schema") - assert "invalid" not in self.warcli(f"graph validate {Path(self.tf_create)}", False) - assert "invalid" not in self.warcli(f"graph validate {Path(self.tf_import)}", False) - assert "invalid" not in self.warcli(f"graph validate {self.graph_file_path}", False) - - def test_graph_with_optional_services(self): - self.log.info("Testing graph with optional services...") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) - self.wait_for_all_tanks_status(target="running") - self.wait_for_all_edges() - self.warcli("bitcoin rpc 0 getblockcount") - - self.log.info("Checking services...") - self.warcli("network down") - self.wait_for_all_tanks_status(target="stopped") + self.cleanup() - def test_created_graph(self): - self.log.info("Testing created graph...") - self.log.info(self.warcli(f"network start {Path(self.tf_create)} --force")) - self.wait_for_all_tanks_status(target="running") - self.wait_for_all_edges() - self.warcli("bitcoin rpc 0 getblockcount") - self.warcli("network down") - self.wait_for_all_tanks_status(target="stopped") - - def test_imported_graph(self): - self.log.info("Testing imported graph...") - self.log.info(self.warcli(f"network start {Path(self.tf_import)} --force")) + def directory_not_exist(self): + try: + self.log.info("testing warnet create, dir doesn't exist") + self.sut = pexpect.spawn("warnet create") + self.sut.expect("init", timeout=10) + except Exception as e: + print(f"\nReceived prompt text:\n {self.sut.before.decode('utf-8')}\n") + raise e + + def directory_exists(self): + try: + self.log.info("testing warnet create, dir does exist") + self.sut = pexpect.spawn("warnet create", encoding="utf-8") + self.sut.logfile = sys.stdout + self.sut.expect("name", timeout=30) + self.sut.sendline("ANewNetwork") + self.sut.expect("many", timeout=30) + self.sut.sendline("") + self.sut.expect("version", timeout=30) + self.sut.sendline("") + self.sut.expect("connections", timeout=30) + self.sut.sendline("") + self.sut.expect("many", timeout=30) + # Up arrow three times: [12] -> 8 -> 4 -> 0 (done) + self.sut.sendline("\x1b[A" * 3) + self.sut.expect("enable fork-observer", timeout=30) + self.sut.sendline("") + self.sut.expect("seconds", timeout=30) + self.sut.sendline("") + self.sut.expect("enable grafana", timeout=30) + self.sut.sendline("true") + self.sut.expect("successfully", timeout=50) + except Exception as e: + print(f"\nReceived prompt text:\n {self.sut.before.decode('utf-8')}\n") + raise e + + def run_created_network(self): + self.log.info("adding custom config to one tank") + with open("networks/ANewNetwork/network.yaml") as f: + s = f.read() + s = s.replace(" name: tank-0000\n", " name: tank-0000\n config: debug=mempool\n") + with open("networks/ANewNetwork/network.yaml", "w") as f: + f.write(s) + + self.log.info("deploying new network") + stream_command("warnet deploy networks/ANewNetwork") self.wait_for_all_tanks_status(target="running") - self.wait_for_all_edges() - self.warcli("bitcoin rpc 0 getblockcount") - self.warcli("scenarios run ln_init") - self.wait_for_all_scenarios() - - self.verify_ln_channel_policies() - - def verify_ln_channel_policies(self): - self.log.info("Ensuring warnet LN channel policies match imported JSON description") - with open(self.json_file_path) as file: - actual = json.loads(self.warcli("ln rpc 0 describegraph"))["edges"] - expected = json.loads(file.read())["edges"] - expected = sorted(expected, key=lambda chan: int(chan["channel_id"])) - for chan_index, actual_chan_json in enumerate(actual): - expected_chan = LNDNode.lnchannel_from_json(expected[chan_index]) - actual_chan = LNDNode.lnchannel_from_json(actual_chan_json) - if not expected_chan.channel_match(actual_chan): - self.log.info( - f"Channel {chan_index} policy mismatch, testing flipped channel: {actual_chan.short_chan_id}" - ) - if not expected_chan.channel_match(actual_chan.flip()): - raise Exception( - f"Channel policy doesn't match source: {actual_chan.short_chan_id}\n" - + f"Actual:\n{actual_chan}\n" - + f"Expected:\n{expected_chan}\n" - ) + debugs = json.loads(self.warnet("bitcoin rpc tank-0000 logging")) + # set in defaultConfig + assert debugs["rpc"] + # set in config just for this tank + assert debugs["mempool"] + # santy check + assert not debugs["zmq"] + # verify that prometheus exporter is making its rpc calls + self.wait_for_predicate( + lambda: "method=getblockcount user=user" in self.warnet("logs tank-0000") + ) if __name__ == "__main__": diff --git a/test/ln_basic_test.py b/test/ln_basic_test.py new file mode 100755 index 000000000..d2ebf13bb --- /dev/null +++ b/test/ln_basic_test.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 + +import json +import os +import subprocess +from pathlib import Path +from time import sleep + +from test_base import TestBase + +from warnet.process import stream_command + + +class LNBasicTest(TestBase): + def __init__(self): + super().__init__() + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "ln" + self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" + self.lns = [ + "tank-0000-ln", + "tank-0001-ln", + "tank-0002-ln", + "tank-0003-ln", + "tank-0004-ln", + "tank-0005-ln", + ] + + 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 pyln-proto package in scenario + self.test_pyln_scenario() + + # Test manually configured macroons + self.test_admin_macaroons() + + # 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") + + # Manually open two more channels between first three nodes + # and send a payment using warnet RPC + self.manual_open_channels() + self.wait_for_gossip_sync(self.lns[:3], 2 + 2) + self.pay_invoice(sender="tank-0000-ln", recipient="tank-0002-ln") + + finally: + self.cleanup() + + def setup_network(self): + self.log.info("Setting up network") + stream_command(f"warnet deploy {self.network_dir}") + + def test_pyln_scenario(self): + self.log.info("Running pyln_connect scenario") + scenario_file = self.scen_dir / "test_scenarios" / "pyln_connect.py" + self.log.info(f"Running scenario from: {scenario_file}") + stream_command(f"warnet run {scenario_file} --source_dir={self.scen_dir} --debug") + + def test_admin_macaroons(self): + self.log.info("Testing lnd nodes with same macaroon root key can query each other") + # These tanks all use the same default macaroon root key, meaning the macaroons + # generated at ~/.lnd/.../admin.macaroon in each lnd container are authorized + # to make requests to each other. + info = json.loads( + self.warnet("ln rpc tank-0001-ln --rpcserver=tank-0002-ln.default:10009 getinfo") + ) + assert info["alias"] == "tank-0002-ln.default" + info = json.loads( + self.warnet("ln rpc tank-0002-ln --rpcserver=tank-0005-ln.default:10009 getinfo") + ) + assert info["alias"] == "tank-0005-ln.default" + + self.log.info("Testing lnd nodes with unique macaroon root key can NOT query each other") + # These tanks are configured with unique macaroon root keys + try: + self.warnet("ln rpc tank-0001-ln --rpcserver=tank-0003-ln.default:10009 getinfo") + raise AssertionError("That should not have worked!") + except Exception as e: + assert "verification failed: signature mismatch after caveat verification" in str(e) + try: + self.warnet("ln rpc tank-0001-ln --rpcserver=tank-0004-ln.default:10009 getinfo") + raise AssertionError("That should not have worked!") + except Exception as e: + assert "verification failed: signature mismatch after caveat verification" in str(e) + try: + self.warnet("ln rpc tank-0003-ln --rpcserver=tank-0004-ln.default:10009 getinfo") + raise AssertionError("That should not have worked!") + except Exception as e: + assert "verification failed: signature mismatch after caveat verification" in str(e) + + def fund_wallets(self): + for ln in self.lns: + if ln == "tank-0000-ln": + # cln + addr = json.loads(self.warnet(f"ln rpc {ln} newaddr p2tr"))["p2tr"] + else: + # lnd + addr = json.loads(self.warnet(f"ln rpc {ln} newaddress p2tr"))["address"] + self.warnet(f"bitcoin rpc tank-0000 sendtoaddress {addr} 10") + self.wait_for_predicate( + lambda: ( + json.loads(self.warnet("bitcoin rpc tank-0000 getmempoolinfo"))["size"] + == len(self.lns) + ) + ) + self.warnet("bitcoin rpc tank-0000 -generate 1") + # cln takes a long time to register its own balance? + self.wait_for_predicate( + lambda: ( + len( + json.loads(self.warnet("ln rpc tank-0000-ln bkpr-listbalances"))["accounts"][0][ + "balances" + ] + ) + > 0 + ) + ) + + def manual_open_channels(self): + self.fund_wallets() + # 0 -> 1 -> 2 + pk1 = self.warnet("ln pubkey tank-0001-ln") + pk2 = self.warnet("ln pubkey tank-0002-ln") + + host1 = "" + host2 = "" + + while not host1 or not host2: + if not host1: + host1 = self.warnet("ln host tank-0001-ln") + if not host2: + host2 = self.warnet("ln host tank-0002-ln") + sleep(1) + + print(self.warnet(f"ln rpc tank-0000-ln connect {pk1} {host1}")) + print(self.warnet(f"ln rpc tank-0000-ln fundchannel {pk1} 100000")) + + print( + self.warnet( + f"ln rpc tank-0001-ln openchannel --node_key {pk2} --local_amt 100000 --connect {host2}" + ) + ) + + self.wait_for_predicate( + lambda: json.loads(self.warnet("bitcoin rpc tank-0000 getmempoolinfo"))["size"] == 2 + ) + + self.warnet("bitcoin rpc tank-0000 -generate 10") + + def wait_for_gossip_sync(self, nodes, expected): + while len(nodes) > 0: + for node in nodes: + if node == "tank-0000-ln": + # cln + chs = json.loads(self.warnet(f"ln rpc {node} listchannels"))["channels"] + chs = [ch for ch in chs if ch["direction"] == 1] + else: + # lnd + chs = json.loads(self.warnet(f"ln rpc {node} describegraph"))["edges"] + if len(chs) >= expected: + nodes.remove(node) + sleep(1) + + def pay_invoice(self, sender: str, recipient: str): + init_balance = int(json.loads(self.warnet(f"ln rpc {recipient} channelbalance"))["balance"]) + inv = json.loads(self.warnet(f"ln rpc {recipient} addinvoice --amt 1000")) + print(inv) + if sender == "tank-0000-ln": + # cln + print(self.warnet(f"ln rpc {sender} pay {inv['payment_request']}")) + else: + # lnd + print(self.warnet(f"ln rpc {sender} payinvoice -f {inv['payment_request']}")) + + def wait_for_success(): + return ( + int(json.loads(self.warnet(f"ln rpc {recipient} channelbalance"))["balance"]) + == init_balance + 1000 + ) + + self.wait_for_predicate(wait_for_success) + + def test_circuit_breaker_api(self): + self.log.info("Testing Circuit Breaker API with direct kubectl commands") + + # Test /info endpoint + info_cmd = f"kubectl exec {self.cb_node} -c circuitbreaker -- wget -qO - 127.0.0.1:{self.cb_port}/api/info" + info = json.loads(subprocess.check_output(info_cmd, shell=True).decode()) + assert "nodeKey" in info, "Circuit breaker info missing nodeKey" + self.log.info(f"Got node info: {info}") + + # Test /limits endpoint + limits_cmd = f"kubectl exec {self.cb_node} -c circuitbreaker -- wget -qO - 127.0.0.1:{self.cb_port}/api/limits" + limits = json.loads(subprocess.check_output(limits_cmd, shell=True).decode()) + assert "limits" in limits, "Circuit breaker limits missing" + self.log.info(f"Got limits: {limits}") + + self.log.info("✅ Circuit Breaker API tests passed") + + +if __name__ == "__main__": + test = LNBasicTest() + test.run_test() diff --git a/test/ln_test.py b/test/ln_test.py index 31e8be112..ee27b6256 100755 --- a/test/ln_test.py +++ b/test/ln_test.py @@ -1,125 +1,122 @@ #!/usr/bin/env python3 - +import ast import json import os from pathlib import Path +from typing import Optional from test_base import TestBase -from warnet.services import ServiceType + +from warnet.k8s import wait_for_pod +from warnet.process import run_command, stream_command class LNTest(TestBase): def __init__(self): super().__init__() - self.graph_file_path = Path(os.path.dirname(__file__)) / "data" / "ln.graphml" + self.graph_file = Path(os.path.dirname(__file__)) / "data" / "LN_10.json" + self.imported_network_dir = self.tmpdir / "imported_network" + self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" + self.plugins_dir = Path(os.path.dirname(__file__)).parent / "resources" / "plugins" + self.simln_exec = Path("simln/plugin.py") def run_test(self): - self.start_server() try: + self.import_network() self.setup_network() - self.run_ln_init_scenario() self.test_channel_policies() - self.test_ln_payment_0_to_2() - self.test_ln_payment_2_to_0() - self.test_simln() + self.test_payments() + self.run_simln() finally: - self.stop_server() + self.cleanup() + + def import_network(self): + self.log.info("Importing network graph from JSON...") + res = self.warnet(f"import-network {self.graph_file} {self.imported_network_dir}") + self.log.info(f"\n{res}") def setup_network(self): - self.log.info("Setting up network") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) - self.wait_for_all_tanks_status(target="running") - self.wait_for_all_edges() - - def get_cb_forwards(self, index): - cmd = "wget -q -O - 127.0.0.1:9235/api/forwarding_history" - res = self.wait_for_rpc( - "exec_run", [index, ServiceType.CIRCUITBREAKER.value, cmd, self.network_name] - ) - return json.loads(res) - - def run_ln_init_scenario(self): - self.log.info("Running LN Init scenario") - self.warcli("bitcoin rpc 0 getblockcount") - self.warcli("scenarios run ln_init") - self.wait_for_all_scenarios() - scenario_return_code = self.get_scenario_return_code("ln_init") - if scenario_return_code != 0: - raise Exception("LN Init scenario failed") + self.log.info("Setting up network...") + stream_command(f"warnet deploy {self.imported_network_dir}") def test_channel_policies(self): self.log.info("Ensuring node-level channel policy settings") - node2pub, node2host = json.loads(self.warcli("ln rpc 2 getinfo"))["uris"][0].split("@") - chan_id = json.loads(self.warcli("ln rpc 2 listchannels"))["channels"][0]["chan_id"] - chan = json.loads(self.warcli(f"ln rpc 2 getchaninfo {chan_id}")) - - # node_1 or node_2 is tank 2 with its non-default --bitcoin.timelockdelta=33 - if chan["node1_policy"]["time_lock_delta"] != 33: - assert ( - chan["node2_policy"]["time_lock_delta"] == 33 - ), "Expected time_lock_delta to be 33" - - self.log.info("Ensuring no circuit breaker forwards yet") - assert len(self.get_cb_forwards(1)["forwards"]) == 0, "Expected no circuit breaker forwards" - - def test_ln_payment_0_to_2(self): - self.log.info("Test LN payment from 0 -> 2") - inv = json.loads(self.warcli("ln rpc 2 addinvoice --amt=2000"))["payment_request"] - self.log.info(f"Got invoice from node 2: {inv}") - self.log.info("Paying invoice from node 0...") - self.log.info(self.warcli(f"ln rpc 0 payinvoice -f {inv}")) - - self.wait_for_predicate(self.check_invoice_settled) - - self.log.info("Ensuring channel-level channel policy settings: source") - payment = json.loads(self.warcli("ln rpc 0 listpayments"))["payments"][0] - assert ( - payment["fee_msat"] == "5506" - ), f"Expected fee_msat to be 5506, got {payment['fee_msat']}" - - self.log.info("Ensuring circuit breaker tracked payment") - assert len(self.get_cb_forwards(1)["forwards"]) == 1, "Expected one circuit breaker forward" - - def test_ln_payment_2_to_0(self): - self.log.info("Test LN payment from 2 -> 0") - inv = json.loads(self.warcli("ln rpc 0 addinvoice --amt=1000"))["payment_request"] - self.log.info(f"Got invoice from node 0: {inv}") - self.log.info("Paying invoice from node 2...") - self.log.info(self.warcli(f"ln rpc 2 payinvoice -f {inv}")) - - self.wait_for_predicate(lambda: self.check_invoices(0) == 1) - - self.log.info("Ensuring channel-level channel policy settings: target") - payment = json.loads(self.warcli("ln rpc 2 listpayments"))["payments"][0] - assert ( - payment["fee_msat"] == "2213" - ), f"Expected fee_msat to be 2213, got {payment['fee_msat']}" - - def test_simln(self): - self.log.info("Engaging simln") - node2pub, _ = json.loads(self.warcli("ln rpc 2 getinfo"))["uris"][0].split("@") - activity = [ - {"source": "ln-0", "destination": node2pub, "interval_secs": 1, "amount_msat": 2000} - ] - self.warcli( - f"network export --exclude=[1] --activity={json.dumps(activity).replace(' ', '')}" - ) - self.wait_for_predicate(lambda: self.check_invoices(2) > 1) - assert self.check_invoices(0) == 1, "Expected one invoice for node 0" - assert self.check_invoices(1) == 0, "Expected no invoices for node 1" - - def check_invoice_settled(self): - invs = json.loads(self.warcli("ln rpc 2 listinvoices"))["invoices"] - if len(invs) > 0 and invs[0]["state"] == "SETTLED": - self.log.info("Invoice settled") - return True - return False - - def check_invoices(self, index): - invs = json.loads(self.warcli(f"ln rpc {index} listinvoices"))["invoices"] - settled = sum(1 for inv in invs if inv["state"] == "SETTLED") - self.log.debug(f"Node {index} has {settled} settled invoices") - return settled + graphs = [] + for n in range(10): + ln = f"tank-{n:04d}-ln" + res = self.warnet(f"ln rpc {ln} describegraph") + graphs.append(json.loads(res)["edges"]) + + def check_policy(node: int, index: int, field: str, values: tuple): + self.log.info(f"Checking policy: Node={node} ch={index} Expected={field}:{values}") + graph = graphs[node] + assert len(graph) == 13 + ch = graph[index] + a = int(ch["node1_policy"][field]) + b = int(ch["node2_policy"][field]) + assert values == (a, b) or values == ( + b, + a, + ), f"policy check failed:\nActual:\n{ch}\nExpected:\n{field}:{values}" + + # test one property of one channel from each node + check_policy(0, 0, "fee_base_msat", (250, 1000)) + check_policy(1, 1, "time_lock_delta", (40, 100)) + check_policy(2, 2, "fee_rate_milli_msat", (1, 4000)) + check_policy(3, 3, "fee_rate_milli_msat", (499, 4000)) + check_policy(4, 4, "time_lock_delta", (40, 144)) + check_policy(5, 5, "max_htlc_msat", (1980000000, 1500000000)) + check_policy(6, 6, "fee_rate_milli_msat", (550, 71)) + check_policy(7, 7, "min_htlc", (1000, 1)) + check_policy(8, 8, "time_lock_delta", (80, 144)) + check_policy(9, 9, "fee_base_msat", (616, 1000)) + + def test_payments(self): + def get_and_pay(src, tgt): + src = f"tank-{src:04d}-ln" + tgt = f"tank-{tgt:04d}-ln" + invoice = json.loads(self.warnet(f"ln rpc {tgt} addinvoice --amt 230118"))[ + "payment_request" + ] + print(self.warnet(f"ln rpc {src} payinvoice {invoice} --force")) + + get_and_pay(0, 5) + get_and_pay(2, 3) + get_and_pay(1, 9) + get_and_pay(8, 7) + get_and_pay(4, 6) + + def run_simln(self): + self.log.info("Running SimLN...") + activity_cmd = f"{self.plugins_dir}/{self.simln_exec} get-example-activity" + activity = run_command(activity_cmd) + launch_cmd = f"{self.plugins_dir}/{self.simln_exec} launch-activity '{activity}'" + pod = run_command(launch_cmd).strip() + wait_for_pod(pod) + self.log.info("Checking SimLN...") + self.wait_for_predicate(self.found_results_remotely) + self.log.info("SimLN was successful.") + + def found_results_remotely(self, pod: Optional[str] = None) -> bool: + if pod is None: + pod = self.get_first_simln_pod() + self.log.info(f"Checking for results file in {pod}") + results_file = run_command( + f"{self.plugins_dir}/{self.simln_exec} sh {pod} ls /working/results" + ).strip() + self.log.info(f"Results file: {results_file}") + results = run_command( + f"{self.plugins_dir}/{self.simln_exec} sh {pod} cat /working/results/{results_file}" + ).strip() + self.log.info(results) + return results.find("Success") > 0 + + def get_first_simln_pod(self): + command = f"{self.plugins_dir}/{self.simln_exec} list-pod-names" + pod_names_literal = run_command(command) + self.log.info(f"{command}: {pod_names_literal}") + pod_names = ast.literal_eval(pod_names_literal) + return pod_names[0] if __name__ == "__main__": diff --git a/test/local_test_runner.py b/test/local_test_runner.py new file mode 100755 index 000000000..0fe744ed2 --- /dev/null +++ b/test/local_test_runner.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +import subprocess +import sys + +import yaml + +# developers can use this tool to verify all repo workflow test execute to completion +# +# execute from repo root like this to execute all tests +# python test/local_test_runner.py +# +# add optional argument to just execute any matching tests +# python test/local_test_runner.py [ln_ | graph] + + +def has_key_path(d, key_path, separator="."): + """Check if a nested key path (dotted notation) exists in a dictionary.""" + keys = key_path.split(separator) + for key in keys: + if not isinstance(d, dict) or key not in d: + return False + d = d[key] + return True + + +# Load the workflow file +with open(".github/workflows/test.yml") as file: + workflow = yaml.safe_load(file) + +tests_total = 0 +tests_skipped = 0 +tests_completed = 0 + +for job_details in workflow.get("jobs", {}).values(): + if has_key_path(job_details, "strategy.matrix.test"): + print("Found test strategy job, starting serial execution of each test") + tests = job_details["strategy"]["matrix"]["test"] + + for test in tests: + tests_total += 1 + if len(sys.argv) > 1 and sys.argv[1] not in test: + print("skipping test as requested:", test) + tests_skipped += 1 + continue + command = f"python test/{test}" + print( + "###################################################################################################" + ) + print("############## executing:", command) + print( + "###################################################################################################" + ) + process = subprocess.run(command, shell=True) + if process.returncode != 0: + print("******** testing failed") + if process.stdout: + print("stdout:", process.stdout) + if process.stderr: + print("stderr:", process.stderr) + sys.exit(1) + tests_completed += 1 + +print( + "###################################################################################################" +) +print("testing complete") +print(f"{tests_completed} of {tests_total} complete - skipped: {tests_skipped} tests") +print( + "###################################################################################################" +) diff --git a/test/logging_test.py b/test/logging_test.py index 2b7797970..230a55d41 100755 --- a/test/logging_test.py +++ b/test/logging_test.py @@ -1,66 +1,51 @@ #!/usr/bin/env python3 -import logging import os -import threading from datetime import datetime from pathlib import Path -from subprocess import PIPE, Popen, run import requests from test_base import TestBase +from warnet.k8s import get_ingress_ip_or_host + class LoggingTest(TestBase): def __init__(self): super().__init__() - self.graph_file_path = Path(os.path.dirname(__file__)) / "data" / "logging.graphml" + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "logging" self.scripts_dir = Path(os.path.dirname(__file__)) / ".." / "resources" / "scripts" - self.connect_logging_process = None - self.connect_logging_thread = None - self.connect_logging_logger = logging.getLogger("cnct_log") def run_test(self): - self.start_server() try: - self.start_logging() self.setup_network() + self.wait_for_endpoint_ready() self.test_prometheus_and_grafana() finally: - if self.connect_logging_process is not None: - self.log.info("Terminating background connect_logging.sh process...") - self.connect_logging_process.terminate() - self.stop_server() - - def start_logging(self): - self.log.info("Running install_logging.sh") - # Block until complete - run([f"{self.scripts_dir / 'install_logging.sh'}"]) - self.log.info("Running connect_logging.sh") - # Stays alive in background - self.connect_logging_process = Popen( - [f"{self.scripts_dir / 'connect_logging.sh'}"], - stdout=PIPE, - stderr=PIPE, - bufsize=1, - universal_newlines=True, - ) - self.log.info("connect_logging.sh started...") - self.connect_logging_thread = threading.Thread( - target=self.output_reader, - args=(self.connect_logging_process.stdout, self.connect_logging_logger.info), - ) - self.connect_logging_thread.daemon = True - self.connect_logging_thread.start() - - self.log.info("Waiting for RPC") - self.wait_for_rpc("scenarios_available") + self.cleanup() def setup_network(self): self.log.info("Setting up network") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) + self.log.info(self.warnet(f"deploy {self.network_dir}")) self.wait_for_all_tanks_status(target="running", timeout=10 * 60) self.wait_for_all_edges() + self.wait_for_predicate(lambda: get_ingress_ip_or_host()) + ingress_ip = get_ingress_ip_or_host() + self.grafana_url = f"http://{ingress_ip}/grafana" + self.log.info(f"Grafana URL: {self.grafana_url}") + + def wait_for_endpoint_ready(self): + self.log.info("Waiting for Grafana to be ready to receive API calls...") + + def check_endpoint(): + try: + response = requests.get(f"{self.grafana_url}/login") + return response.status_code == 200 + except requests.RequestException: + return False + + self.wait_for_predicate(check_endpoint, timeout=120) + self.log.info("Grafana login endpoint returned status code 200") def make_grafana_api_request(self, ds_uid, start, metric): self.log.info("Making Grafana request...") @@ -69,18 +54,24 @@ def make_grafana_api_request(self, ds_uid, start, metric): "from": f"{start}", "to": "now", } - reply = requests.post("http://localhost:3000/api/ds/query", json=data) - assert reply.status_code == 200 + reply = requests.post(f"{self.grafana_url}/api/ds/query", json=data) + if reply.status_code != 200: + self.log.error(f"Grafana API request failed with status code {reply.status_code}") + self.log.error(f"Response content: {reply.text}") + return None # Default ref ID is "A", only inspecting one "frame" return reply.json()["results"]["A"]["frames"][0]["data"]["values"] def test_prometheus_and_grafana(self): self.log.info("Starting network activity scenarios") - self.warcli("scenarios run miner_std --allnodes --interval=5 --mature") - self.warcli("scenarios run tx_flood --interval=1") - prometheus_ds = requests.get("http://localhost:3000/api/datasources/name/Prometheus") + miner_file = "resources/scenarios/miner_std.py" + tx_flood_file = "resources/scenarios/tx_flood.py" + self.warnet(f"run {miner_file} --allnodes --interval=5 --mature") + self.warnet(f"run {tx_flood_file} --interval=1") + + prometheus_ds = requests.get(f"{self.grafana_url}/api/datasources/name/Prometheus") assert prometheus_ds.status_code == 200 prometheus_uid = prometheus_ds.json()["uid"] self.log.info(f"Got Prometheus data source uid from Grafana: {prometheus_uid}") @@ -89,6 +80,9 @@ def test_prometheus_and_grafana(self): def get_five_values_for_metric(metric): data = self.make_grafana_api_request(prometheus_uid, start, metric) + if data is None: + self.log.info(f"Failed to get Grafana data for {metric}") + return False if len(data) < 1: self.log.info(f"No Grafana data yet for {metric}") return False @@ -100,6 +94,11 @@ def get_five_values_for_metric(metric): self.wait_for_predicate(lambda: get_five_values_for_metric("blocks")) self.wait_for_predicate(lambda: get_five_values_for_metric("txrate")) + self.wait_for_predicate(lambda: get_five_values_for_metric("lnd_block_height")) + + # Verify default dashboard exists + dbs = requests.get(f"{self.grafana_url}/api/search").json() + assert dbs[0]["title"] == "Default Warnet Dashboard" if __name__ == "__main__": diff --git a/test/namespace_admin_test.py b/test/namespace_admin_test.py new file mode 100755 index 000000000..1431cc65d --- /dev/null +++ b/test/namespace_admin_test.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 + +import os +from pathlib import Path +from typing import Callable, Optional + +from scenarios_test import ScenariosTest +from test_base import TestBase + +from warnet.constants import KUBECONFIG, WARGAMES_NAMESPACE_PREFIX +from warnet.k8s import ( + K8sError, + get_kubeconfig_value, + get_static_client, + open_kubeconfig, + write_kubeconfig, +) +from warnet.process import run_command + + +class NamespaceAdminTest(ScenariosTest, TestBase): + def __init__(self): + super().__init__() + + self.namespace_dir = ( + Path(os.path.dirname(__file__)) + / "data" + / "admin" + / "namespaces" + / "two_namespaces_two_users" + ) + + self.initial_context = None + self.current_context = None + self.bob_user = "bob-warnettest" + self.bob_auth_file = "bob-warnettest-wargames-red-team-warnettest-kubeconfig" + self.bob_context = "bob-warnettest-wargames-red-team-warnettest" + + self.blue_namespace = "wargames-blue-team-warnettest" + self.red_namespace = "wargames-red-team-warnettest" + self.blue_users = ["carol-warnettest", "default", "mallory-warnettest"] + self.red_users = ["alice-warnettest", self.bob_user, "default"] + + def run_test(self): + try: + os.chdir(self.tmpdir) + self.log.info(f"Running test in: {self.tmpdir}") + self.establish_initial_context() + self.setup_namespaces() + self.setup_service_accounts() + self.setup_network() + self.authenticate_and_become_bob() + self.bob_runs_scenario_tests() + finally: + self.return_to_initial_context() + try: + self.cleanup_kubeconfig() + except K8sError as e: + self.log.info(f"KUBECONFIG cleanup error: {e}") + self.cleanup() + + def establish_initial_context(self): + self.initial_context = get_kubeconfig_value("{.current-context}") + self.log.info(f"Initial context: {self.initial_context}") + self.current_context = self.initial_context + self.log.info(f"Current context: {self.current_context}") + + def setup_namespaces(self): + self.log.info("Setting up the namespaces") + self.log.info(self.warnet(f"deploy {self.namespace_dir}")) + self.wait_for_predicate(self.two_namespaces_are_validated) + self.log.info("Namespace setup complete") + + def setup_service_accounts(self): + self.log.info("Creating service accounts...") + self.log.info(self.warnet("admin create-kubeconfigs")) + self.wait_for_predicate(self.service_accounts_are_validated) + self.log.info("Service accounts have been set up and validated") + + def setup_network(self): + if self.current_context == self.bob_context: + self.log.info(f"Allowing {self.current_context} to update the network...") + assert self.this_is_the_current_context(self.bob_context) + self.warnet(f"deploy {self.network_dir}") + else: + self.log.info("Deploy networks to team namespaces") + assert self.this_is_the_current_context(self.initial_context) + self.log.info(self.warnet(f"deploy {self.network_dir} --to-all-users")) + self.wait_for_all_tanks_status() + self.log.info("Waiting for all edges") + self.wait_for_all_edges() + + def authenticate_and_become_bob(self): + self.log.info("Authenticating and becoming bob...") + self.log.info(f"Current context: {self.current_context}") + assert self.initial_context == self.current_context + assert get_kubeconfig_value("{.current-context}") == self.initial_context + self.warnet(f"auth kubeconfigs/{self.bob_auth_file}") + self.current_context = self.bob_context + assert get_kubeconfig_value("{.current-context}") == self.current_context + self.log.info(f"Current context: {self.current_context}") + + def service_accounts_are_validated(self) -> bool: + self.log.info("Checking service accounts") + sclient = get_static_client() + namespaces = sclient.list_namespace().items + + filtered_namespaces = [ + ns.metadata.name + for ns in namespaces + if ns.metadata.name.startswith(WARGAMES_NAMESPACE_PREFIX) + ] + assert len(filtered_namespaces) != 0 + + maybe_service_accounts = {} + + for namespace in filtered_namespaces: + service_accounts = sclient.list_namespaced_service_account(namespace=namespace).items + for sa in service_accounts: + maybe_service_accounts.setdefault(namespace, []).append(sa.metadata.name) + + expected = { + self.blue_namespace: self.blue_users, + self.red_namespace: self.red_users, + } + + return maybe_service_accounts == expected + + def get_namespaces(self) -> Optional[list[str]]: + self.log.info("Querying the namespaces...") + resp = self.warnet("admin namespaces list") + if resp == "No warnet namespaces found.": + return None + namespaces = [] + for line in resp.splitlines(): + if line.startswith("- "): + namespaces.append(line.lstrip("- ")) + self.log.info(f"Namespaces: {namespaces}") + return namespaces + + def two_namespaces_are_validated(self) -> bool: + maybe_namespaces = self.get_namespaces() + if maybe_namespaces is None: + return False + if self.blue_namespace not in maybe_namespaces: + return False + return self.red_namespace in maybe_namespaces + + def return_to_initial_context(self): + self.log.info(self.warnet("auth --revert")) + self.wait_for_predicate(self.this_is_the_current_context(self.initial_context)) + + def this_is_the_current_context(self, context: str) -> Callable[[], bool]: + cmd = "kubectl config current-context" + current_context = run_command(cmd).strip() + self.log.info(f"Current context: {current_context} {context == current_context}") + return lambda: current_context == context + + def cleanup_kubeconfig(self): + try: + kubeconfig_data = open_kubeconfig(KUBECONFIG) + except K8sError as e: + raise K8sError(f"Could not open KUBECONFIG: {KUBECONFIG}") from e + + kubeconfig_data = remove_user(kubeconfig_data, self.bob_user) + kubeconfig_data = remove_context(kubeconfig_data, self.bob_context) + + try: + write_kubeconfig(kubeconfig_data, KUBECONFIG) + except Exception as e: + raise K8sError(f"Could not write to KUBECONFIG: {KUBECONFIG}") from e + + def bob_runs_scenario_tests(self): + assert self.this_is_the_current_context(self.bob_context) + super().run_test() + assert self.this_is_the_current_context(self.bob_context) + + +def remove_user(kubeconfig_data: dict, username: str) -> dict: + kubeconfig_data["users"] = [ + user for user in kubeconfig_data["users"] if user["name"] != username + ] + return kubeconfig_data + + +def remove_context(kubeconfig_data: dict, context_name: str) -> dict: + kubeconfig_data["contexts"] = [ + context for context in kubeconfig_data["contexts"] if context["name"] != context_name + ] + return kubeconfig_data + + +if __name__ == "__main__": + test = NamespaceAdminTest() + test.run_test() diff --git a/test/onion_test.py b/test/onion_test.py index 7f7454b60..75ec6b049 100755 --- a/test/onion_test.py +++ b/test/onion_test.py @@ -6,64 +6,75 @@ from test_base import TestBase +from warnet.k8s import pod_log + class OnionTest(TestBase): def __init__(self): super().__init__() - self.graph_file_path = Path(os.path.dirname(__file__)) / "data" / "12_node_ring.graphml" - self.onion_addr = None + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "onion" def run_test(self): - self.start_server() try: self.setup_network() - self.test_reachability() - self.test_onion_peer_connection() + self.check_tor() finally: - self.stop_server() + self.cleanup() def setup_network(self): self.log.info("Setting up network") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) + self.log.info(self.warnet(f"deploy {self.network_dir}")) self.wait_for_all_tanks_status(target="running") - self.wait_for_all_edges() - def test_reachability(self): - self.log.info("Checking IPv4 and onion reachability") - self.wait_for_predicate(self.check_reachability, timeout=10 * 60) + def check_tor(self): + onions = {"tank-0001": None, "tank-0002": None} - def check_reachability(self): - try: - info = json.loads(self.warcli("bitcoin rpc 0 getnetworkinfo")) - for net in info["networks"]: - if net["name"] == "ipv4" and not net["reachable"]: - return False - if net["name"] == "onion" and not net["reachable"]: - return False - if len(info["localaddresses"]) != 2: - return False - for addr in info["localaddresses"]: - assert "100." in addr["address"] or ".onion" in addr["address"] - if ".onion" in addr["address"]: - self.onion_addr = addr["address"] - return True - except Exception as e: - self.log.error(f"Error checking reachability: {e}") - return False - - def test_onion_peer_connection(self): - self.log.info("Attempting addnode to onion peer") - self.warcli(f"bitcoin rpc 1 addnode {self.onion_addr} add") - # Might take up to 10 minutes - self.wait_for_predicate(self.check_onion_peer, timeout=10 * 60) - - def check_onion_peer(self): - peers = json.loads(self.warcli("bitcoin rpc 0 getpeerinfo")) - for peer in peers: - self.log.debug(f"Checking peer: {peer['network']} {peer['addr']}") - if peer["network"] == "onion": + def get_onions(): + peers = ["tank-0001", "tank-0002"] + for tank in peers: + if not onions[tank]: + self.log.info(f"Getting local onion address from {tank}...") + info = json.loads(self.warnet(f"bitcoin rpc {tank} getnetworkinfo")) + for addr in info["localaddresses"]: + if "onion" in addr["address"]: + onions[tank] = addr["address"] + self.log.info(f" ... got: {addr['address']}") + return all(onions[tank] for tank in peers) + + self.wait_for_predicate(get_onions) + + self.log.info("Adding 1 block") + self.warnet("bitcoin rpc tank-0001 createwallet miner") + self.warnet("bitcoin rpc tank-0001 -generate 1") + + self.log.info("Adding connections via onion: 0000->0001->0002") + self.warnet(f"bitcoin rpc tank-0000 addnode {onions['tank-0001']} add") + self.warnet(f"bitcoin rpc tank-0001 addnode {onions['tank-0002']} add") + + def onion_connect(): + peers = json.loads(self.warnet("bitcoin rpc tank-0001 getpeerinfo")) + self.log.info("\n") + self.log.info("Waiting for tank-0001 to have at least two onion peers:") + self.log.info(json.dumps(peers, indent=2)) + if len(peers) >= 2: + for peer in peers: + assert peer["network"] == "onion" return True - return False + else: + self.log.info("tank-0001 tor log tail:") + stream = pod_log( + pod_name="tank-0001", + container_name="tor", + namespace="default", + follow=False, + tail_lines=5, + ) + for line in stream: + msg = line.decode("utf-8").rstrip() + msg = msg.split("]") + self.log.info(msg[-1]) + + self.wait_for_predicate(onion_connect, timeout=20 * 60) if __name__ == "__main__": diff --git a/test/plugin_test.py b/test/plugin_test.py new file mode 100755 index 000000000..a0358a585 --- /dev/null +++ b/test/plugin_test.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +import ast +import os +from functools import partial +from pathlib import Path +from typing import Optional + +from test_base import TestBase + +from warnet.k8s import download, wait_for_pod +from warnet.process import run_command + + +class PluginTest(TestBase): + def __init__(self): + super().__init__() + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "network_with_plugins" + self.plugins_dir = Path(os.path.dirname(__file__)).parent / "resources" / "plugins" + self.simln_exec = self.plugins_dir / "simln" / "plugin.py" + + def run_test(self): + try: + os.chdir(self.tmpdir) + self.deploy_with_plugin() + self.copy_results() + self.assert_hello_plugin() + finally: + self.cleanup() + + def deploy_with_plugin(self): + self.log.info("Deploy the ln network with a SimLN plugin") + results = self.warnet(f"deploy {self.network_dir}") + self.log.info(results) + wait_for_pod(self.get_first_simln_pod()) + + def copy_results(self): + pod = self.get_first_simln_pod() + partial_func = partial(self.found_results_remotely, pod) + self.wait_for_predicate(partial_func) + + download(pod, Path("/working/results"), Path(".")) + self.wait_for_predicate(self.found_results_locally) + + def found_results_remotely(self, pod: Optional[str] = None) -> bool: + if pod is None: + pod = self.get_first_simln_pod() + self.log.info(f"Checking for results file in {pod}") + results_file = run_command(f"{self.simln_exec} sh {pod} ls /working/results").strip() + self.log.info(f"Results file: {results_file}") + results = run_command( + f"{self.simln_exec} sh {pod} cat /working/results/{results_file}" + ).strip() + self.log.info(results) + return results.find("Success") > 0 + + def get_first_simln_pod(self): + command = f"{self.simln_exec} list-pod-names" + pod_names_literal = run_command(command) + self.log.info(f"{command}: {pod_names_literal}") + pod_names = ast.literal_eval(pod_names_literal) + return pod_names[0] + + def found_results_locally(self) -> bool: + directory = "results" + self.log.info(f"Searching {directory}") + for root, _dirs, files in os.walk(Path(directory)): + for file_name in files: + file_path = os.path.join(root, file_name) + + with open(file_path) as file: + content = file.read() + if "Success" in content: + self.log.info(f"Found downloaded results in directory: {directory}.") + return True + self.log.info(f"Did not find downloaded results in directory: {directory}.") + return False + + def assert_hello_plugin(self): + self.log.info("Waiting for the 'hello' plugin pods.") + wait_for_pod("hello-pre-deploy") + wait_for_pod("hello-post-deploy") + wait_for_pod("hello-pre-network") + wait_for_pod("hello-post-network") + wait_for_pod("tank-0000-post-hello-pod") + wait_for_pod("tank-0000-pre-hello-pod") + wait_for_pod("tank-0001-post-hello-pod") + wait_for_pod("tank-0001-pre-hello-pod") + wait_for_pod("tank-0002-post-hello-pod") + wait_for_pod("tank-0002-pre-hello-pod") + wait_for_pod("tank-0003-post-hello-pod") + wait_for_pod("tank-0003-pre-hello-pod") + wait_for_pod("tank-0004-post-hello-pod") + wait_for_pod("tank-0004-pre-hello-pod") + wait_for_pod("tank-0005-post-hello-pod") + wait_for_pod("tank-0005-pre-hello-pod") + + +if __name__ == "__main__": + test = PluginTest() + test.run_test() diff --git a/test/rpc_test.py b/test/rpc_test.py index 642151e5f..ba466ea53 100755 --- a/test/rpc_test.py +++ b/test/rpc_test.py @@ -10,10 +10,9 @@ class RPCTest(TestBase): def __init__(self): super().__init__() - self.graph_file_path = Path(os.path.dirname(__file__)) / "data" / "12_node_ring.graphml" + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "12_node_ring" def run_test(self): - self.start_server() try: self.setup_network() self.test_rpc_commands() @@ -21,44 +20,44 @@ def run_test(self): self.test_message_exchange() self.test_address_manager() finally: - self.stop_server() + self.cleanup() def setup_network(self): self.log.info("Setting up network") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) + self.log.info(self.warnet(f"deploy {self.network_dir}")) self.wait_for_all_tanks_status(target="running") self.wait_for_all_edges() def test_rpc_commands(self): self.log.info("Testing basic RPC commands") - self.warcli("bitcoin rpc 0 getblockcount") - self.warcli("bitcoin rpc 1 createwallet miner") - self.warcli("bitcoin rpc 1 -generate 101") - self.wait_for_predicate(lambda: "101" in self.warcli("bitcoin rpc 0 getblockcount")) + self.warnet("bitcoin rpc tank-0000 getblockcount") + self.warnet("bitcoin rpc tank-0001 createwallet miner") + self.warnet("bitcoin rpc tank-0001 -generate 101") + self.wait_for_predicate(lambda: "101" in self.warnet("bitcoin rpc tank-0000 getblockcount")) def test_transaction_propagation(self): self.log.info("Testing transaction propagation") address = "bcrt1qthmht0k2qnh3wy7336z05lu2km7emzfpm3wg46" - txid = self.warcli(f"bitcoin rpc 1 sendtoaddress {address} 0.1") - self.wait_for_predicate(lambda: txid in self.warcli("bitcoin rpc 0 getrawmempool")) + txid = self.warnet(f"bitcoin rpc tank-0001 sendtoaddress {address} 0.1") + self.wait_for_predicate(lambda: txid in self.warnet("bitcoin rpc tank-0000 getrawmempool")) - node_log = self.warcli("bitcoin debug-log 1") + node_log = self.warnet("bitcoin debug-log tank-0001") assert txid in node_log, "Transaction ID not found in node log" - all_logs = self.warcli(f"bitcoin grep-logs {txid}") + all_logs = self.warnet(f"bitcoin grep-logs {txid}") count = all_logs.count("Enqueuing TransactionAddedToMempool") assert count > 1, f"Transaction not propagated to enough nodes (count: {count})" def test_message_exchange(self): self.log.info("Testing message exchange between nodes") - msgs = self.warcli("bitcoin messages 0 1") + msgs = self.warnet("bitcoin messages tank-0000 tank-0001") assert "verack" in msgs, "VERACK message not found in exchange" def test_address_manager(self): self.log.info("Testing address manager") def got_addrs(): - addrman = json.loads(self.warcli("bitcoin rpc 0 getrawaddrman")) + addrman = json.loads(self.warnet("bitcoin rpc tank-0000 getrawaddrman")) for key in ["tried", "new"]: obj = addrman[key] keys = list(obj.keys()) diff --git a/test/scenarios_test.py b/test/scenarios_test.py index 734b2cae4..9d89cf8ed 100755 --- a/test/scenarios_test.py +++ b/test/scenarios_test.py @@ -1,109 +1,136 @@ #!/usr/bin/env python3 import os +import re from pathlib import Path from test_base import TestBase +from warnet.control import stop_scenario +from warnet.process import run_command +from warnet.status import _get_deployed_scenarios as scenarios_deployed + class ScenariosTest(TestBase): def __init__(self): super().__init__() - self.graph_file_path = Path(os.path.dirname(__file__)) / "data" / "12_node_ring.graphml" + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "12_node_ring" + self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" def run_test(self): try: - self.start_server() + self.check_help() self.setup_network() - self.test_scenarios() + self.run_and_check_miner_scenario_from_file() + self.run_and_check_scenario_from_file() + self.run_and_check_scenario_from_file_debug() + self.check_regtest_recon() + self.check_active_count() finally: - self.stop_server() + self.cleanup() def setup_network(self): self.log.info("Setting up network") - self.log.info(self.warcli(f"network start {self.graph_file_path}")) + self.log.info(self.warnet(f"deploy {self.network_dir}")) self.wait_for_all_tanks_status(target="running") - - def test_scenarios(self): - self.check_available_scenarios() - self.run_and_check_miner_scenario("miner_std") - self.run_and_check_miner_scenario_from_file("src/warnet/scenarios/miner_std.py") - self.run_and_check_scenario_from_file("test/data/scenario_p2p_interface.py") - - def check_available_scenarios(self): - self.log.info("Checking available scenarios") - # Use rpc instead of warcli so we get raw JSON object - scenarios = self.rpc("scenarios_available") - assert len(scenarios) == 4, f"Expected 4 available scenarios, got {len(scenarios)}" - self.log.info(f"Found {len(scenarios)} available scenarios") + self.wait_for_all_edges() def scenario_running(self, scenario_name: str): """Check that we are only running a single scenario of the correct name""" - active = self.rpc("scenarios_list_running") - running = scenario_name in active[0]["cmd"] - return running and len(active) == 1 + deployed = scenarios_deployed() + assert len(deployed) == 1 + return scenario_name in deployed[0]["name"] + + def check_scenario_stopped(self): + running = scenarios_deployed() + self.log.debug(f"Checking if scenario stopped. Running scenarios: {len(running)}") + return len(running) == 0 - def run_and_check_scenario_from_file(self, scenario_file): - scenario_name = self.get_scenario_name_from_path(scenario_file) + def check_scenario_clean_exit(self): + deployed = scenarios_deployed() + return all(scenario["status"] == "succeeded" for scenario in deployed) - def check_scenario_clean_exit(): - running = self.rpc("scenarios_list_running") - scenarios = [s for s in running if s["cmd"].strip() == scenario_name] - if not scenarios: - return False - scenario = scenarios[0] - if scenario["active"]: - return False - if scenario["return_code"] != 0: - raise Exception( - f"Scenario {scenario_name} failed with return code {scenario['return_code']}" - ) - return True - - self.log.info(f"Running scenario: {scenario_name}") - self.warcli(f"scenarios run-file {scenario_file}") - self.wait_for_predicate(lambda: check_scenario_clean_exit()) - - def run_and_check_miner_scenario(self, scenario_name): - self.log.info(f"Running scenario: {scenario_name}") - self.warcli(f"scenarios run {scenario_name} --allnodes --interval=1") - self.wait_for_predicate(lambda: self.scenario_running(scenario_name)) - self.wait_for_predicate(lambda: self.check_blocks(30)) - self.stop_scenario() + def stop_scenario(self): + self.log.info("Stopping running scenario") + running = scenarios_deployed() + assert len(running) == 1, f"Expected one running scenario, got {len(running)}" + assert running[0]["status"] == "running", "Scenario should be running" + stop_scenario(running[0]["name"]) + self.wait_for_predicate(self.check_scenario_stopped) + + def check_blocks(self, target_blocks, start: int = 0): + count = int(self.warnet("bitcoin rpc tank-0000 getblockcount")) + self.log.debug(f"Current block count: {count}, target: {start + target_blocks}") + + try: + deployed = scenarios_deployed() + commander = deployed[0]["commander"] + command = f"kubectl logs {commander}" + print("\ncommander output:") + print(run_command(command)) + print("\n") + except Exception: + pass - def run_and_check_miner_scenario_from_file(self, scenario_file): + return count >= start + target_blocks + + def check_help(self): + scenario_file = self.scen_dir / "miner_std.py" + self.log.info(f"Running scenario from file with -- --help: {scenario_file}") + help_text = self.warnet(f"run {scenario_file} -- --help") + assert "usage" in help_text + # no commander pods actually deployed + assert len(scenarios_deployed()) == 0 + + def run_and_check_miner_scenario_from_file(self): + scenario_file = self.scen_dir / "miner_std.py" self.log.info(f"Running scenario from file: {scenario_file}") - self.warcli(f"scenarios run-file {scenario_file} --allnodes --interval=1") - start = int(self.warcli("bitcoin rpc 0 getblockcount")) - scenario_name = self.get_scenario_name_from_path(scenario_file) - self.wait_for_predicate(lambda: self.scenario_running(scenario_name)) + self.warnet(f"run {scenario_file} --allnodes --interval=1") + start = int(self.warnet("bitcoin rpc tank-0000 getblockcount")) + self.wait_for_predicate(lambda: self.scenario_running("commander-minerstd")) self.wait_for_predicate(lambda: self.check_blocks(2, start=start)) + table = self.warnet("status") + assert "Active Scenarios: 1" in table self.stop_scenario() - def get_scenario_name_from_path(self, scenario_file): - return os.path.splitext(os.path.basename(scenario_file))[0] + def run_and_check_scenario_from_file_debug(self): + scenario_file = self.scen_dir / "test_scenarios" / "p2p_interface.py" + self.log.info(f"Running scenario from: {scenario_file}") + output = self.warnet(f"run {scenario_file} --source_dir={self.scen_dir} --debug") + self.check_for_pod_deletion_message(output) - def check_blocks(self, target_blocks, start: int = 0): - running = self.rpc("scenarios_list_running") - assert len(running) == 1, f"Expected one running scenario, got {len(running)}" - assert running[0]["active"], "Scenario should be active" + def run_and_check_scenario_from_file(self): + scenario_file = self.scen_dir / "test_scenarios" / "p2p_interface.py" + self.log.info(f"Running scenario from: {scenario_file}") + self.warnet(f"run {scenario_file} --source_dir={self.scen_dir}") + self.wait_for_predicate(self.check_scenario_clean_exit) - count = int(self.warcli("bitcoin rpc 0 getblockcount")) - self.log.debug(f"Current block count: {count}, target: {start + target_blocks}") - return count >= start + target_blocks + def check_regtest_recon(self): + scenario_file = self.scen_dir / "reconnaissance.py" + self.log.info(f"Running scenario from file: {scenario_file}") + self.warnet(f"run {scenario_file}") + self.wait_for_predicate(self.check_scenario_clean_exit) - def stop_scenario(self): - self.log.info("Stopping running scenario") - running = self.rpc("scenarios_list_running") - assert len(running) == 1, f"Expected one running scenario, got {len(running)}" - assert running[0]["active"], "Scenario should be active" - self.warcli(f"scenarios stop {running[0]['pid']}", False) - self.wait_for_predicate(self.check_scenario_stopped) + def check_active_count(self): + scenario_file = self.scen_dir / "test_scenarios" / "buggy_failure.py" + self.log.info(f"Running scenario from: {scenario_file}") + self.warnet(f"run {scenario_file} --source_dir={self.scen_dir}") - def check_scenario_stopped(self): - running = self.rpc("scenarios_list_running") - self.log.debug(f"Checking if scenario stopped. Running scenarios: {len(running)}") - return len(running) == 0 + def two_pass_one_fail(): + deployed = scenarios_deployed() + if len([s for s in deployed if s["status"] == "succeeded"]) != 2: + return False + return len([s for s in deployed if s["status"] == "failed"]) == 1 + + self.wait_for_predicate(two_pass_one_fail) + table = self.warnet("status") + assert "Active Scenarios: 0" in table + + def check_for_pod_deletion_message(self, input): + message = "Deleting pod..." + self.log.info(f"Checking for message: '{message}'") + assert re.search(re.escape(message), input, flags=re.MULTILINE) + self.log.info(f"Found message: '{message}'") if __name__ == "__main__": diff --git a/test/services_test.py b/test/services_test.py new file mode 100755 index 000000000..87f0a6f3f --- /dev/null +++ b/test/services_test.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +import json +import os +from pathlib import Path +from time import sleep + +import requests +from test_base import TestBase + +from warnet.k8s import get_ingress_ip_or_host, wait_for_ingress_controller + + +class ServicesTest(TestBase): + def __init__(self): + super().__init__() + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "services" + self.ingress_ip = None + + def run_test(self): + try: + self.setup_network() + self.get_ingress_ip() + self.check_fork_observer() + self.check_extra_services() + finally: + self.cleanup() + + def setup_network(self): + self.log.info("Setting up network") + self.log.info(self.warnet(f"deploy {self.network_dir}")) + self.wait_for_all_tanks_status(target="running") + self.wait_for_all_edges() + + def get_ingress_ip(self): + self.log.info("Waiting for ingress controller") + wait_for_ingress_controller() + self.log.info("Waiting for ingress host") + attempts = 100 + while not self.ingress_ip: + self.ingress_ip = get_ingress_ip_or_host() + attempts -= 1 + if attempts < 0: + raise Exception("Never got ingress host") + sleep(1) + + def check_fork_observer(self): + self.log.info("Creating chain split") + self.warnet("bitcoin rpc john createwallet miner") + self.warnet("bitcoin rpc john -generate 1") + + # network id is 0xDEADBE in decimal + fo_data_uri = f"http://{self.ingress_ip}/fork-observer/api/14593470/data.json" + + def call_fo_api(): + # if on minikube remember to run `minikube tunnel` for this test to run + try: + self.log.info(f"Getting: {fo_data_uri}") + fo_data = requests.get(fo_data_uri) + # fork observed! + return len(fo_data.json()["header_infos"]) == 2 + except Exception as e: + self.log.info(f"Fork Observer API error: {e}") + self.log.info("No Fork observed yet") + return False + + self.wait_for_predicate(call_fo_api) + self.log.info("Fork observed!") + + self.log.info("Checking node description...") + fo_data = requests.get(fo_data_uri) + nodes = fo_data.json()["nodes"] + assert len(nodes) == 4 + assert nodes[1]["name"] == "john" + assert nodes[1]["description"] == "john.default.svc:18444" + + self.log.info("Checking reachable address is provided...") + self.warnet("bitcoin rpc george addnode john.default.svc:18444 onetry") + self.wait_for_predicate( + lambda: len(json.loads(self.warnet("bitcoin rpc george getpeerinfo"))) > 1 + ) + + def check_extra_services(self): + self.log.info("Checking extra web services added to caddy") + uri = f"http://{self.ingress_ip}/ringo/rest/chaininfo.json" + rest_data = requests.get(uri) + rest_json = rest_data.json() + assert rest_json["chain"] == "regtest" + + +if __name__ == "__main__": + test = ServicesTest() + test.run_test() diff --git a/test/signet_test.py b/test/signet_test.py new file mode 100755 index 000000000..726fdd3a7 --- /dev/null +++ b/test/signet_test.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 + +import json +import os +from pathlib import Path + +from test_base import TestBase + +from warnet.status import _get_deployed_scenarios as scenarios_deployed + + +class SignetTest(TestBase): + def __init__(self): + super().__init__() + self.network_dir = Path(os.path.dirname(__file__)) / "data" / "signet" + signer_data_path = Path(os.path.dirname(__file__)) / "data" / "signet-signer.json" + with open(signer_data_path) as f: + self.signer_data = json.loads(f.read()) + + def run_test(self): + try: + self.setup_network() + self.check_signet_recon() + self.check_signet_scenario_miner() + finally: + self.cleanup() + + def setup_network(self): + self.log.info("Setting up network") + self.log.info(self.warnet(f"deploy {self.network_dir}")) + self.wait_for_all_tanks_status(target="running") + self.wait_for_all_edges() + + def check_signet_recon(self): + scenario_file = "resources/scenarios/reconnaissance.py" + self.log.info(f"Running scenario from file: {scenario_file}") + self.warnet(f"run {scenario_file}") + + def check_scenario_clean_exit(): + deployed = scenarios_deployed() + return all(scenario["status"] == "succeeded" for scenario in deployed) + + self.wait_for_predicate(check_scenario_clean_exit) + + def check_signet_scenario_miner(self): + self.warnet("bitcoin rpc miner createwallet miner") + self.warnet( + f"bitcoin rpc miner importdescriptors {json.dumps(self.signer_data['descriptors'])}" + ) + before_count = int(self.warnet("bitcoin rpc tank-1 getblockcount")) + + self.log.info("Generate 1 signet block from a scenario using the bitcoin-util grinder") + self.scen_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" + scenario_file = self.scen_dir / "test_scenarios" / "signet_grinder.py" + self.log.info(f"Running scenario from: {scenario_file}") + self.warnet(f"run {scenario_file} --source_dir={self.scen_dir} --admin") + self.wait_for_all_scenarios() + + after_count = int(self.warnet("bitcoin rpc tank-1 getblockcount")) + assert after_count - before_count == 1 + + deployed = scenarios_deployed() + found = False + for sc in deployed: + if "grinder" in sc["name"]: + found = True + log = self.warnet(f"logs {sc['name']}") + assert "Error grinding" not in log + assert found + + +if __name__ == "__main__": + test = SignetTest() + test.run_test() diff --git a/test/test_base.py b/test/test_base.py index 5ad3c9125..59a026256 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -1,26 +1,28 @@ -import atexit import json import logging import logging.config import os import re +import sys import threading from pathlib import Path -from subprocess import PIPE, Popen, run +from subprocess import run from tempfile import mkdtemp from time import sleep +import pexpect + from warnet import SRC_DIR -from warnet.cli.rpc import rpc_call -from warnet.utils import exponential_backoff -from warnet.warnet import Warnet +from warnet.k8s import get_pod_exit_status +from warnet.network import _connected as network_connected +from warnet.status import _get_deployed_scenarios as scenarios_deployed +from warnet.status import _get_tank_status as network_status class TestBase: def __init__(self): self.setup_environment() self.setup_logging() - atexit.register(self.cleanup) self.log_expected_msgs: None | [str] = None self.log_unexpected_msgs: None | [str] = None self.log_msg_assertions_passed = False @@ -30,11 +32,6 @@ def setup_environment(self): self.tmpdir = Path(mkdtemp(prefix="warnet-test-")) os.environ["XDG_STATE_HOME"] = str(self.tmpdir) self.logfilepath = self.tmpdir / "warnet.log" - # Use the same dir name for the warnet network name - # replacing underscores which throws off k8s - self.network_name = self.tmpdir.name.replace("_", "") - self.server = None - self.server_thread = None self.stop_threads = threading.Event() self.network = True @@ -45,23 +42,21 @@ def setup_logging(self): logging.config.dictConfig(logging_config) self.log = logging.getLogger("test") self.log.info("Logging started") + self.log.info(f"Testdir: {self.tmpdir}") def cleanup(self, signum=None, frame=None): - if self.server is None: - return try: self.log.info("Stopping network") if self.network: - self.warcli("network down") + session = pexpect.spawn("warnet down", encoding="utf-8") + session.logfile = sys.stdout + session.expect("Do you want to bring down the running Warnet?", timeout=30) + session.sendline("y") self.wait_for_all_tanks_status(target="stopped", timeout=60, interval=1) except Exception as e: self.log.error(f"Error bringing network down: {e}") finally: self.stop_threads.set() - self.server.terminate() - self.server.wait() - self.server_thread.join() - self.server = None def _print_and_assert_msgs(self, message): print(message) @@ -71,74 +66,33 @@ def _print_and_assert_msgs(self, message): self.log_msg_assertions_passed = True def assert_log_msgs(self): - assert ( - self.log_msg_assertions_passed - ), f"Log assertion failed. Expected message not found: {self.log_expected_msgs}" + assert self.log_msg_assertions_passed, ( + f"Log assertion failed. Expected message not found: {self.log_expected_msgs}" + ) self.log_msg_assertions_passed = False - def warcli(self, cmd, network=True): - self.log.debug(f"Executing warcli command: {cmd}") - command = ["warcli"] + cmd.split() - if network: - command += ["--network", self.network_name] + def warnet(self, cmd): + self.log.debug(f"Executing warnet command: {cmd}") + command = ["warnet"] + cmd.split() proc = run(command, capture_output=True) if proc.stderr: raise Exception(proc.stderr.decode().strip()) return proc.stdout.decode().strip() - def rpc(self, method, params=None) -> dict | list: - """Execute a warnet RPC API call directly""" - self.log.debug(f"Executing RPC method: {method}") - return rpc_call(method, params) - - @exponential_backoff(max_retries=20) - def wait_for_rpc(self, method, params=None): - """Repeatedly execute an RPC until it succeeds""" - return self.rpc(method, params) - def output_reader(self, pipe, func): while not self.stop_threads.is_set(): line = pipe.readline().strip() if line: func(line) - def start_server(self): - """Start the Warnet server and wait for RPC interface to respond""" - - if self.server is not None: - raise Exception("Server is already running") - - # TODO: check for conflicting warnet process - # maybe also ensure that no conflicting docker networks exist - - # For kubernetes we assume the server is started outside test base, - # but we can still read its log output - self.log.info("Starting Warnet server") - self.server = Popen( - ["kubectl", "logs", "-f", "rpc-0", "--since=1s"], - stdout=PIPE, - stderr=PIPE, - bufsize=1, - universal_newlines=True, - ) - - self.server_thread = threading.Thread( - target=self.output_reader, args=(self.server.stdout, self._print_and_assert_msgs) - ) - self.server_thread.daemon = True - self.server_thread.start() - - self.log.info("Waiting for RPC") - self.wait_for_rpc("scenarios_available") - - def stop_server(self): - self.cleanup() - def wait_for_predicate(self, predicate, timeout=5 * 60, interval=5): self.log.debug(f"Waiting for predicate with timeout {timeout}s and interval {interval}s") while timeout > 0: - if predicate(): - return + try: + if predicate(): + return + except Exception: + pass sleep(interval) timeout -= interval import inspect @@ -148,8 +102,8 @@ def wait_for_predicate(self, predicate, timeout=5 * 60, interval=5): ) def get_tank(self, index): - wn = Warnet.from_network(self.network_name) - return wn.tanks[index] + # TODO + return None def wait_for_all_tanks_status(self, target="running", timeout=20 * 60, interval=5): """Poll the warnet server for container status @@ -157,14 +111,15 @@ def wait_for_all_tanks_status(self, target="running", timeout=20 * 60, interval= """ def check_status(): - tanks = self.wait_for_rpc("network_status", {"network": self.network_name}) + tanks = network_status() stats = {"total": 0} + # "Probably" means all tanks are stopped and deleted + if len(tanks) == 0: + return True for tank in tanks: - for service in ["bitcoin", "lightning", "circuitbreaker"]: - status = tank.get(f"{service}_status") - if status: - stats["total"] += 1 - stats[status] = stats.get(status, 0) + 1 + status = tank["status"] + stats["total"] += 1 + stats[status] = stats.get(status, 0) + 1 self.log.info(f"Waiting for all tanks to reach '{target}': {stats}") return target in stats and stats[target] == stats["total"] @@ -174,26 +129,22 @@ def wait_for_all_edges(self, timeout=20 * 60, interval=5): """Ensure all tanks have all the connections they are supposed to have Block until all success """ - - def check_status(): - return self.wait_for_rpc("network_connected", {"network": self.network_name}) - - self.wait_for_predicate(check_status, timeout, interval) + self.wait_for_predicate(network_connected, timeout, interval) def wait_for_all_scenarios(self): def check_scenarios(): - scns = self.rpc("scenarios_list_running") - return all(not scn["active"] for scn in scns) + scns = scenarios_deployed() + if len(scns) == 0: + return True + for s in scns: + exit_status = get_pod_exit_status(s["name"], s["namespace"]) + self.log.debug(f"Scenario {s['name']} exited with code {exit_status}") + if exit_status != 0: + return False + return True self.wait_for_predicate(check_scenarios) - def get_scenario_return_code(self, scenario_name): - scns = self.rpc("scenarios_list_running") - scns = [scn for scn in scns if scn["cmd"].strip() == scenario_name] - if len(scns) == 0: - raise Exception(f"Scenario {scenario_name} not found in running scenarios") - return scns[0]["return_code"] - def assert_equal(thing1, thing2, *args): if thing1 != thing2 or any(thing1 != arg for arg in args): diff --git a/test/wargames_test.py b/test/wargames_test.py new file mode 100755 index 000000000..c304c51ac --- /dev/null +++ b/test/wargames_test.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +import os +from pathlib import Path + +import pexpect +from test_base import TestBase + +from warnet.k8s import get_kubeconfig_value +from warnet.process import stream_command +from warnet.status import _get_deployed_scenarios as scenarios_deployed + + +class WargamesTest(TestBase): + def __init__(self): + super().__init__() + self.wargame_dir = Path(os.path.dirname(__file__)) / "data" / "wargames" + self.scen_src_dir = Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" + self.scen_test_dir = ( + Path(os.path.dirname(__file__)).parent / "resources" / "scenarios" / "test_scenarios" + ) + self.initial_context = get_kubeconfig_value("{.current-context}") + self.kconfigdir = self.tmpdir / "kubeconfigs" + + def run_test(self): + try: + self.setup_battlefield() + self.setup_armies() + self.check_scenario_permissions() + finally: + self.log.info("Restoring initial_context") + stream_command(f"kubectl config use-context {self.initial_context}") + self.cleanup() + + def setup_battlefield(self): + self.log.info("Setting up battlefield") + self.log.info(self.warnet(f"deploy {self.wargame_dir / 'networks' / 'battlefield'}")) + self.wait_for_all_tanks_status(target="running") + self.wait_for_all_edges() + + def setup_armies(self): + self.log.info("Deploying namespaces and armadas") + self.log.info(self.warnet(f"deploy {self.wargame_dir / 'namespaces' / 'armies'}")) + self.log.info( + self.warnet(f"deploy {self.wargame_dir / 'networks' / 'armada'} --to-all-users") + ) + self.wait_for_all_tanks_status(target="running") + self.wait_for_all_edges() + + def check_scenario_permissions(self): + self.log.info("Admin without --admin can not command a node outside of default namespace") + stream_command( + f"warnet run {self.scen_test_dir / 'generate_one_allnodes.py'} --source_dir={self.scen_src_dir} --debug" + ) + # Only miner.default and target-red.default were accesible + assert self.warnet("bitcoin rpc miner getblockcount") == "2" + + self.log.info("Admin with --admin can command all nodes in any namespace") + stream_command( + f"warnet run {self.scen_test_dir / 'generate_one_allnodes.py'} --source_dir={self.scen_src_dir} --admin --debug" + ) + # armada.wargames-red, miner.default and target-red.default were accesible + assert self.warnet("bitcoin rpc miner getblockcount") == "5" + + self.log.info("Switch to wargames player context") + self.log.info(self.warnet(f"admin create-kubeconfigs --kubeconfig-dir={self.kconfigdir}")) + clicker = pexpect.spawn( + f"warnet auth {self.kconfigdir}/warnet-user-wargames-red-kubeconfig" + ) + while clicker.expect(["Overwrite", "Updated kubeconfig"]) == 0: + print(clicker.before, clicker.after) + clicker.sendline("y") + print(clicker.before, clicker.after) + + self.log.info("Player without --admin can only command the node inside their own namespace") + stream_command( + f"warnet run {self.scen_test_dir / 'generate_one_allnodes.py'} --source_dir={self.scen_src_dir} --debug" + ) + # Only armada.wargames-red was (and is) accesible + assert self.warnet("bitcoin rpc armada getblockcount") == "6" + + self.log.info("Player attempting to use --admin is gonna have a bad time") + stream_command( + f"warnet run {self.scen_test_dir / 'generate_one_allnodes.py'} --source_dir={self.scen_src_dir} --admin --debug" + ) + # Nothing was accesible + assert self.warnet("bitcoin rpc armada getblockcount") == "6" + + self.log.info("Check pod limit per namespace") + for _ in range(10): + stream_command( + f"warnet run {self.scen_test_dir / 'nothing.py'} --source_dir={self.scen_src_dir}" + ) + assert len(scenarios_deployed()) == 2, ( + f"Unexpected scenarios deployed:{scenarios_deployed()}" + ) + + self.log.info("Restore admin context") + stream_command(f"kubectl config use-context {self.initial_context}") + # Sanity check + assert self.warnet("bitcoin rpc miner getblockcount") == "6" + + self.log.info("Re-generate kubeconfigs after a scenario has been run") + self.log.info(self.warnet(f"admin create-kubeconfigs --kubeconfig-dir={self.kconfigdir}")) + count = 0 + for p in self.kconfigdir.iterdir(): + if p.is_file(): + print(p) + assert "warnet-user" in str(p) + count += 1 + assert count <= 1 + + +if __name__ == "__main__": + test = WargamesTest() + test.run_test() diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..586994544 --- /dev/null +++ b/uv.lock @@ -0,0 +1,904 @@ +version = 1 +requires-python = ">=3.11" + +[[package]] +name = "ansicon" +version = "1.89.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/e2/1c866404ddbd280efedff4a9f15abfe943cb83cde6e895022370f3a61f85/ansicon-1.89.0.tar.gz", hash = "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1", size = 67312 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/f9/f1c10e223c7b56a38109a3f2eb4e7fe9a757ea3ed3a166754fb30f65e466/ansicon-1.89.0-py2.py3-none-any.whl", hash = "sha256:f1def52d17f65c2c9682cf8370c03f541f410c1752d6a14029f97318e4b9dfec", size = 63675 }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, +] + +[[package]] +name = "blessed" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinxed", marker = "platform_system == 'Windows'" }, + { name = "six" }, + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/ae/92e9968ad23205389ec6bd82e2d4fca3817f1cdef34e10aa8d529ef8b1d7/blessed-1.20.0.tar.gz", hash = "sha256:2cdd67f8746e048f00df47a2880f4d6acbcdb399031b604e34ba8f71d5787680", size = 6655612 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/98/584f211c3a4bb38f2871fa937ee0cc83c130de50c955d6c7e2334dbf4acb/blessed-1.20.0-py2.py3-none-any.whl", hash = "sha256:0c542922586a265e699188e52d5f5ac5ec0dd517e5a1041d90d2bbf23f906058", size = 58372 }, +] + +[[package]] +name = "blinker" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/57/a6a1721eff09598fb01f3c7cda070c1b6a0f12d63c83236edf79a440abcc/blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83", size = 23161 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/2a/10164ed1f31196a2f7f3799368a821765c62851ead0e630ab52b8e14b4d0/blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", size = 9456 }, +] + +[[package]] +name = "build" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/9e/2d725d2f7729c6e79ca62aeb926492abbc06e25910dd30139d60a68bcb19/build-1.2.1.tar.gz", hash = "sha256:526263f4870c26f26c433545579475377b2b7588b6f1eac76a001e873ae3e19d", size = 44781 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/03/f3c8ba0a6b6e30d7d18c40faab90807c9bb5e9a1e3b2fe2008af624a9c97/build-1.2.1-py3-none-any.whl", hash = "sha256:75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4", size = 21911 }, +] + +[[package]] +name = "cachetools" +version = "5.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/38/a0f315319737ecf45b4319a8cd1f3a908e29d9277b46942263292115eee7/cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a", size = 27661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/07/14f8ad37f2d12a5ce41206c21820d8cb6561b728e51fad4530dff0552a67/cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", size = 9524 }, +] + +[[package]] +name = "certifi" +version = "2024.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/02/a95f2b11e207f68bc64d7aae9666fed2e2b3f307748d5123dffb72a1bbea/certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", size = 164065 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/d5/c84e1a17bf61d4df64ca866a1c9a913874b4e9bdc131ec689a0ad013fb36/certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90", size = 162960 }, +] + +[[package]] +name = "cffi" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/bf/82c351342972702867359cfeba5693927efe0a8dd568165490144f554b18/cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76", size = 516073 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/cc/9298fb6235522e00e47d78d6aa7f395332ef4e5f6fe124f9a03aa60600f7/cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720", size = 181912 }, + { url = "https://files.pythonhosted.org/packages/e7/79/dc5334fbe60635d0846c56597a8d2af078a543ff22bc48d36551a0de62c2/cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9", size = 178297 }, + { url = "https://files.pythonhosted.org/packages/39/d7/ef1b6b16b51ccbabaced90ff0d821c6c23567fc4b2e4a445aea25d3ceb92/cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb", size = 444909 }, + { url = "https://files.pythonhosted.org/packages/29/b8/6e3c61885537d985c78ef7dd779b68109ba256263d74a2f615c40f44548d/cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424", size = 468854 }, + { url = "https://files.pythonhosted.org/packages/0b/49/adad1228e19b931e523c2731e6984717d5f9e33a2f9971794ab42815b29b/cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d", size = 476890 }, + { url = "https://files.pythonhosted.org/packages/76/54/c00f075c3e7fd14d9011713bcdb5b4f105ad044c5ad948db7b1a0a7e4e78/cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8", size = 459374 }, + { url = "https://files.pythonhosted.org/packages/f3/b9/f163bb3fa4fbc636ee1f2a6a4598c096cdef279823ddfaa5734e556dd206/cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6", size = 466891 }, + { url = "https://files.pythonhosted.org/packages/31/52/72bbc95f6d06ff2e88a6fa13786be4043e542cb24748e1351aba864cb0a7/cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91", size = 477658 }, + { url = "https://files.pythonhosted.org/packages/67/20/d694811457eeae0c7663fa1a7ca201ce495533b646c1180d4ac25684c69c/cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8", size = 453890 }, + { url = "https://files.pythonhosted.org/packages/dc/79/40cbf5739eb4f694833db5a27ce7f63e30a9b25b4a836c4f25fb7272aacc/cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb", size = 478254 }, + { url = "https://files.pythonhosted.org/packages/e9/eb/2c384c385cca5cae67ca10ac4ef685277680b8c552b99aedecf4ea23ff7e/cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9", size = 171285 }, + { url = "https://files.pythonhosted.org/packages/ca/42/74cb1e0f1b79cb64672f3cb46245b506239c1297a20c0d9c3aeb3929cb0c/cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0", size = 180842 }, + { url = "https://files.pythonhosted.org/packages/1a/1f/7862231350cc959a3138889d2c8d33da7042b22e923457dfd4cd487d772a/cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc", size = 182826 }, + { url = "https://files.pythonhosted.org/packages/8b/8c/26119bf8b79e05a1c39812064e1ee7981e1f8a5372205ba5698ea4dd958d/cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59", size = 178494 }, + { url = "https://files.pythonhosted.org/packages/61/94/4882c47d3ad396d91f0eda6ef16d45be3d752a332663b7361933039ed66a/cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb", size = 454459 }, + { url = "https://files.pythonhosted.org/packages/0f/7c/a6beb119ad515058c5ee1829742d96b25b2b9204ff920746f6e13bf574eb/cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195", size = 478502 }, + { url = "https://files.pythonhosted.org/packages/61/8a/2575cd01a90e1eca96a30aec4b1ac101a6fae06c49d490ac2704fa9bc8ba/cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e", size = 485381 }, + { url = "https://files.pythonhosted.org/packages/cd/66/85899f5a9f152db49646e0c77427173e1b77a1046de0191ab3b0b9a5e6e3/cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828", size = 470907 }, + { url = "https://files.pythonhosted.org/packages/00/13/150924609bf377140abe6e934ce0a57f3fc48f1fd956ec1f578ce97a4624/cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150", size = 479074 }, + { url = "https://files.pythonhosted.org/packages/17/fd/7d73d7110155c036303b0a6462c56250e9bc2f4119d7591d27417329b4d1/cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a", size = 484225 }, + { url = "https://files.pythonhosted.org/packages/fc/83/8353e5c9b01bb46332dac3dfb18e6c597a04ceb085c19c814c2f78a8c0d0/cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885", size = 488388 }, + { url = "https://files.pythonhosted.org/packages/73/0c/f9d5ca9a095b1fc88ef77d1f8b85d11151c374144e4606da33874e17b65b/cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492", size = 172096 }, + { url = "https://files.pythonhosted.org/packages/72/21/8c5d285fe20a6e31d29325f1287bb0e55f7d93630a5a44cafdafb5922495/cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2", size = 181478 }, + { url = "https://files.pythonhosted.org/packages/17/8f/581f2f3c3464d5f7cf87c2f7a5ba9acc6976253e02d73804240964243ec2/cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118", size = 182638 }, + { url = "https://files.pythonhosted.org/packages/8d/1c/c9afa66684b7039f48018eb11b229b659dfb32b7a16b88251bac106dd1ff/cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7", size = 178453 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/1a134d479d3a5a1ff2fabbee551d1d3f1dd70f453e081b5f70d604aae4c0/cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377", size = 454441 }, + { url = "https://files.pythonhosted.org/packages/b1/b4/e1569475d63aad8042b0935dbf62ae2a54d1e9142424e2b0e924d2d4a529/cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb", size = 478543 }, + { url = "https://files.pythonhosted.org/packages/d2/40/a9ad03fbd64309dec5bb70bc803a9a6772602de0ee164d7b9a6ca5a89249/cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555", size = 485463 }, + { url = "https://files.pythonhosted.org/packages/a6/1a/f10be60e006dd9242a24bcc2b1cd55c34c578380100f742d8c610f7a5d26/cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204", size = 470854 }, + { url = "https://files.pythonhosted.org/packages/cc/b3/c035ed21aa3d39432bd749fe331ee90e4bc83ea2dbed1f71c4bc26c41084/cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f", size = 479096 }, + { url = "https://files.pythonhosted.org/packages/00/cb/6f7edde01131de9382c89430b8e253b8c8754d66b63a62059663ceafeab2/cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0", size = 484013 }, + { url = "https://files.pythonhosted.org/packages/b9/83/8e4e8c211ea940210d293e951bf06b1bfb90f2eeee590e9778e99b4a8676/cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4", size = 488119 }, + { url = "https://files.pythonhosted.org/packages/5e/52/3f7cfbc4f444cb4f73ff17b28690d12436dde665f67d68f1e1687908ab6c/cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a", size = 172122 }, + { url = "https://files.pythonhosted.org/packages/94/19/cf5baa07ee0f0e55eab7382459fbddaba0fdb0ba45973dd92556ae0d02db/cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7", size = 181504 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", size = 104809 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", size = 191647 }, + { url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", size = 121434 }, + { url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", size = 118979 }, + { url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", size = 136582 }, + { url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", size = 146645 }, + { url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", size = 139398 }, + { url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", size = 140273 }, + { url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", size = 142577 }, + { url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", size = 137747 }, + { url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", size = 143375 }, + { url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", size = 140232 }, + { url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", size = 140859 }, + { url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", size = 92509 }, + { url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", size = 99870 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", size = 192892 }, + { url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", size = 122213 }, + { url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", size = 119404 }, + { url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", size = 137275 }, + { url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", size = 147518 }, + { url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", size = 140182 }, + { url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", size = 141869 }, + { url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", size = 144042 }, + { url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", size = 138275 }, + { url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", size = 144819 }, + { url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", size = 149415 }, + { url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", size = 141212 }, + { url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", size = 142167 }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", size = 93041 }, + { url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", size = 100397 }, + { url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", size = 48543 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cryptography" +version = "43.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/ec/9fb9dcf4f91f0e5e76de597256c43eedefd8423aa59be95c70c4c3db426a/cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e", size = 686873 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/46/dcd2eb6840b9452e7fbc52720f3dc54a85eb41e68414733379e8f98e3275/cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74", size = 6239718 }, + { url = "https://files.pythonhosted.org/packages/e8/23/b0713319edff1d8633775b354f8b34a476e4dd5f4cd4b91e488baec3361a/cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895", size = 3808466 }, + { url = "https://files.pythonhosted.org/packages/77/9d/0b98c73cebfd41e4fb0439fe9ce08022e8d059f51caa7afc8934fc1edcd9/cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22", size = 3998060 }, + { url = "https://files.pythonhosted.org/packages/ae/71/e073795d0d1624847f323481f7d84855f699172a632aa37646464b0e1712/cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47", size = 3792596 }, + { url = "https://files.pythonhosted.org/packages/83/25/439a8ddd8058e7f898b7d27c36f94b66c8c8a2d60e1855d725845f4be0bc/cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf", size = 4008355 }, + { url = "https://files.pythonhosted.org/packages/c7/a2/1607f1295eb2c30fcf2c07d7fd0c3772d21dcdb827de2b2730b02df0af51/cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55", size = 3899133 }, + { url = "https://files.pythonhosted.org/packages/5e/64/f41f42ddc9c583737c9df0093affb92c61de7d5b0d299bf644524afe31c1/cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431", size = 4096946 }, + { url = "https://files.pythonhosted.org/packages/cd/cd/d165adcf3e707d6a049d44ade6ca89973549bed0ab3686fa49efdeefea53/cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc", size = 2616826 }, + { url = "https://files.pythonhosted.org/packages/f9/b7/38924229e84c41b0e88d7a5eed8a29d05a44364f85fbb9ddb3984b746fd2/cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778", size = 3078700 }, + { url = "https://files.pythonhosted.org/packages/66/d7/397515233e6a861f921bd0365b162b38e0cc513fcf4f1bdd9cc7bc5a3384/cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66", size = 6242814 }, + { url = "https://files.pythonhosted.org/packages/58/aa/99b2c00a4f54c60d210d6d1759c720ecf28305aa32d6fb1bb1853f415be6/cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5", size = 3809467 }, + { url = "https://files.pythonhosted.org/packages/76/eb/ab783b47b3b9b55371b4361c7ec695144bde1a3343ff2b7a8c1d8fe617bb/cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e", size = 3998617 }, + { url = "https://files.pythonhosted.org/packages/a3/62/62770f34290ebb1b6542bd3f13b3b102875b90aed4804e296f8d2a5ac6d7/cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5", size = 3794003 }, + { url = "https://files.pythonhosted.org/packages/0f/6c/b42660b3075ff543065b2c1c5a3d9bedaadcff8ebce2ee981be2babc2934/cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f", size = 4008774 }, + { url = "https://files.pythonhosted.org/packages/f7/74/028cea86db9315ba3f991e307adabf9f0aa15067011137c38b2fb2aa16eb/cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0", size = 3900098 }, + { url = "https://files.pythonhosted.org/packages/bd/f6/e4387edb55563e2546028ba4c634522fe727693d3cdd9ec0ecacedc75411/cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b", size = 4096867 }, + { url = "https://files.pythonhosted.org/packages/ce/61/55560405e75432bdd9f6cf72fa516cab623b83a3f6d230791bc8fc4afeee/cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf", size = 2616481 }, + { url = "https://files.pythonhosted.org/packages/e6/3d/696e7a0f04555c58a2813d47aaa78cb5ba863c1f453c74a4f45ae772b054/cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709", size = 3081462 }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, +] + +[[package]] +name = "editor" +version = "1.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "runs" }, + { name = "xmod" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/92/734a4ab345914259cb6146fd36512608ea42be16195375c379046f33283d/editor-1.6.6.tar.gz", hash = "sha256:bb6989e872638cd119db9a4fce284cd8e13c553886a1c044c6b8d8a160c871f8", size = 3197 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/c2/4bc8cd09b14e28ce3f406a8b05761bed0d785d1ca8c2a5c6684d884c66a2/editor-1.6.6-py3-none-any.whl", hash = "sha256:e818e6913f26c2a81eadef503a2741d7cca7f235d20e217274a009ecd5a74abf", size = 4017 }, +] + +[[package]] +name = "flask" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/e1/d104c83026f8d35dfd2c261df7d64738341067526406b40190bc063e829a/flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842", size = 676315 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3", size = 101735 }, +] + +[[package]] +name = "google-auth" +version = "2.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/ae/634dafb151366d91eb848a25846a780dbce4326906ef005d199723fbbca0/google_auth-2.34.0.tar.gz", hash = "sha256:8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc", size = 257875 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fb/9af9e3f2996677bdda72734482934fe85a3abde174e5f0783ac2f817ba98/google_auth-2.34.0-py2.py3-none-any.whl", hash = "sha256:72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65", size = 200870 }, +] + +[[package]] +name = "idna" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/ed/f86a79a07470cb07819390452f178b3bef1d375f2ec021ecfc709fc7cf07/idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", size = 189575 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0", size = 66836 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/bd/fa8ce65b0a7d4b6d143ec23b0f5fd3f7ab80121078c465bc02baeaab22dc/importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5", size = 54320 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/14/362d31bf1076b21e1bcdcb0dc61944822ff263937b804a79231df2774d28/importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1", size = 26269 }, +] + +[[package]] +name = "inquirer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blessed" }, + { name = "editor" }, + { name = "readchar" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/06/ef91eb8f3feafb736aa33dcb278fc9555d17861aa571b684715d095db24d/inquirer-3.4.0.tar.gz", hash = "sha256:8edc99c076386ee2d2204e5e3653c2488244e82cb197b2d498b3c1b5ffb25d0b", size = 14472 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/b2/be907c8c0f8303bc4b10089f5470014c3bf3521e9b8d3decf3037fd94725/inquirer-3.4.0-py3-none-any.whl", hash = "sha256:bb0ec93c833e4ce7b51b98b1644b0a4d2bb39755c39787f6a504e4fee7a11b60", size = 18077 }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, +] + +[[package]] +name = "jaraco-functools" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/b1/6ca3c2052e584e9908a2c146f00378939b3c51b839304ab8ef4de067f042/jaraco_functools-4.0.2.tar.gz", hash = "sha256:3460c74cd0d32bf82b9576bbb3527c4364d5b27a21f5158a62aed6c4b42e23f5", size = 18319 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/54/7623e24ffc63730c3a619101361b08860c6b7c7cfc1aef6edb66d80ed708/jaraco.functools-4.0.2-py3-none-any.whl", hash = "sha256:c9d16a3ed4ccb5a889ad8e0b7a343401ee5b2a71cee6ed192d3f68bc351e94e3", size = 9883 }, +] + +[[package]] +name = "jeepney" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/f4/154cf374c2daf2020e05c3c6a03c91348d59b23c5366e968feb198306fdf/jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", size = 106005 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", size = 48435 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "jinxed" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ansicon", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/d0/59b2b80e7a52d255f9e0ad040d2e826342d05580c4b1d7d7747cfb8db731/jinxed-1.3.0.tar.gz", hash = "sha256:1593124b18a41b7a3da3b078471442e51dbad3d77b4d4f2b0c26ab6f7d660dbf", size = 80981 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/e3/0e0014d6ab159d48189e92044ace13b1e1fe9aa3024ba9f4e8cf172aa7c2/jinxed-1.3.0-py2.py3-none-any.whl", hash = "sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5", size = 33085 }, +] + +[[package]] +name = "keyring" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/30/bfdde7294ba6bb2f519950687471dc6a0996d4f77ab30d75c841fa4994ed/keyring-25.3.0.tar.gz", hash = "sha256:8d85a1ea5d6db8515b59e1c5d1d1678b03cf7fc8b8dcfb1651e8c4a524eb42ef", size = 61495 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/42/ea8c9726e5ee5ff0731978aaf7cd5fa16674cf549c46279b279d7167c2b4/keyring-25.3.0-py3-none-any.whl", hash = "sha256:8d963da00ccdf06e356acd9bf3b743208878751032d8599c6cc89eb51310ffae", size = 38742 }, +] + +[[package]] +name = "kubernetes" +version = "30.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "google-auth" }, + { name = "oauthlib" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/3c/9f29f6cab7f35df8e54f019e5719465fa97b877be2454e99f989270b4f34/kubernetes-30.1.0.tar.gz", hash = "sha256:41e4c77af9f28e7a6c314e3bd06a8c6229ddd787cad684e0ab9f69b498e98ebc", size = 887810 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/2027ddede72d33be2effc087580aeba07e733a7360780ae87226f1f91bd8/kubernetes-30.1.0-py2.py3-none-any.whl", hash = "sha256:e212e8b7579031dd2e512168b617373bc1e03888d41ac4e04039240a292d478d", size = 1706042 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219 }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098 }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014 }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220 }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756 }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988 }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718 }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317 }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670 }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224 }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215 }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069 }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452 }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462 }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869 }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906 }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296 }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038 }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572 }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "more-itertools" +version = "10.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/0d/ad6a82320cb8eba710fd0dceb0f678d5a1b58d67d03ae5be14874baa39e0/more-itertools-10.4.0.tar.gz", hash = "sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923", size = 120755 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/0b/6a51175e1395774449fca317fb8861379b7a2d59be411b8cce3d19d6ce78/more_itertools-10.4.0-py3-none-any.whl", hash = "sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27", size = 60935 }, +] + +[[package]] +name = "nh3" +version = "0.2.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/62/73/10df50b42ddb547a907deeb2f3c9823022580a7a47281e8eae8e003a9639/nh3-0.2.18.tar.gz", hash = "sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4", size = 15028 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/89/1daff5d9ba5a95a157c092c7c5f39b8dd2b1ddb4559966f808d31cfb67e0/nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86", size = 1374474 }, + { url = "https://files.pythonhosted.org/packages/2c/b6/42fc3c69cabf86b6b81e4c051a9b6e249c5ba9f8155590222c2622961f58/nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811", size = 694573 }, + { url = "https://files.pythonhosted.org/packages/45/b9/833f385403abaf0023c6547389ec7a7acf141ddd9d1f21573723a6eab39a/nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200", size = 844082 }, + { url = "https://files.pythonhosted.org/packages/05/2b/85977d9e11713b5747595ee61f381bc820749daf83f07b90b6c9964cf932/nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164", size = 782460 }, + { url = "https://files.pythonhosted.org/packages/72/f2/5c894d5265ab80a97c68ca36f25c8f6f0308abac649aaf152b74e7e854a8/nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189", size = 879827 }, + { url = "https://files.pythonhosted.org/packages/ab/a7/375afcc710dbe2d64cfbd69e31f82f3e423d43737258af01f6a56d844085/nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad", size = 841080 }, + { url = "https://files.pythonhosted.org/packages/c2/a8/3bb02d0c60a03ad3a112b76c46971e9480efa98a8946677b5a59f60130ca/nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b", size = 924144 }, + { url = "https://files.pythonhosted.org/packages/1b/63/6ab90d0e5225ab9780f6c9fb52254fa36b52bb7c188df9201d05b647e5e1/nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307", size = 769192 }, + { url = "https://files.pythonhosted.org/packages/a4/17/59391c28580e2c32272761629893e761442fc7666da0b1cdb479f3b67b88/nh3-0.2.18-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f", size = 791042 }, + { url = "https://files.pythonhosted.org/packages/a3/da/0c4e282bc3cff4a0adf37005fa1fb42257673fbc1bbf7d1ff639ec3d255a/nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe", size = 1010073 }, + { url = "https://files.pythonhosted.org/packages/de/81/c291231463d21da5f8bba82c8167a6d6893cc5419b0639801ee5d3aeb8a9/nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a", size = 1029782 }, + { url = "https://files.pythonhosted.org/packages/63/1d/842fed85cf66c973be0aed8770093d6a04741f65e2c388ddd4c07fd3296e/nh3-0.2.18-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50", size = 942504 }, + { url = "https://files.pythonhosted.org/packages/eb/61/73a007c74c37895fdf66e0edcd881f5eaa17a348ff02f4bb4bc906d61085/nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204", size = 941541 }, + { url = "https://files.pythonhosted.org/packages/78/48/54a788fc9428e481b2f58e0cd8564f6c74ffb6e9ef73d39e8acbeae8c629/nh3-0.2.18-cp37-abi3-win32.whl", hash = "sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be", size = 573750 }, + { url = "https://files.pythonhosted.org/packages/26/8d/53c5b19c4999bdc6ba95f246f4ef35ca83d7d7423e5e38be43ad66544e5d/nh3-0.2.18-cp37-abi3-win_amd64.whl", hash = "sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844", size = 579012 }, +] + +[[package]] +name = "oauthlib" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, +] + +[[package]] +name = "packaging" +version = "24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 }, +] + +[[package]] +name = "pkginfo" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/72/347ec5be4adc85c182ed2823d8d1c7b51e13b9a6b0c1aae59582eca652df/pkginfo-1.10.0.tar.gz", hash = "sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", size = 378457 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/09/054aea9b7534a15ad38a363a2bd974c20646ab1582a387a95b8df1bfea1c/pkginfo-1.10.0-py3-none-any.whl", hash = "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097", size = 30392 }, +] + +[[package]] +name = "pyasn1" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/a3/d2157f333900747f20984553aca98008b6dc843eb62f3a36030140ccec0d/pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", size = 148088 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/7e/5f50d07d5e70a2addbccd90ac2950f81d1edd0783630651d9268d7f1db49/pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473", size = 85313 }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/00/e7bd1dec10667e3f2be602686537969a7ac92b0a7c5165be2e5875dc3971/pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6", size = 307859 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/68/8906226b15ef38e71dc926c321d2fe99de8048e9098b5dfd38343011c886/pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b", size = 181220 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/07/6f63dda440d4abb191b91dc383b472dae3dd9f37e4c1e4a5c3db150531c6/pyproject_hooks-1.1.0.tar.gz", hash = "sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965", size = 7838 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/f3/431b9d5fe7d14af7a32340792ef43b8a714e7726f1d7b69cc4e8e7a3f1d7/pyproject_hooks-1.1.0-py3-none-any.whl", hash = "sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2", size = 9184 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "pywin32" +version = "306" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/1e/fc18ad83ca553e01b97aa8393ff10e33c1fb57801db05488b83282ee9913/pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407", size = 8507689 }, + { url = "https://files.pythonhosted.org/packages/7e/9e/ad6b1ae2a5ad1066dc509350e0fbf74d8d50251a51e420a2a8feaa0cecbd/pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e", size = 9227547 }, + { url = "https://files.pythonhosted.org/packages/91/20/f744bff1da8f43388498503634378dbbefbe493e65675f2cc52f7185c2c2/pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a", size = 10388324 }, + { url = "https://files.pythonhosted.org/packages/14/91/17e016d5923e178346aabda3dfec6629d1a26efe587d19667542105cf0a6/pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b", size = 8507705 }, + { url = "https://files.pythonhosted.org/packages/83/1c/25b79fc3ec99b19b0a0730cc47356f7e2959863bf9f3cd314332bddb4f68/pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e", size = 9227429 }, + { url = "https://files.pythonhosted.org/packages/1c/43/e3444dc9a12f8365d9603c2145d16bf0a2f8180f343cf87be47f5579e547/pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040", size = 10388145 }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "readchar" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/31/2934981710c63afa9c58947d2e676093ce4bb6c7ce60aac2fcc4be7d98d0/readchar-4.2.0.tar.gz", hash = "sha256:44807cbbe377b72079fea6cba8aa91c809982d7d727b2f0dbb2d1a8084914faa", size = 9691 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/6f/ca076ad4d18b3d33c31c304fb7e68dd9ce2bfdb49fb8874611ad7c55e969/readchar-4.2.0-py3-none-any.whl", hash = "sha256:2a587a27c981e6d25a518730ad4c88c429c315439baa6fda55d7a8b3ac4cb62a", size = 9349 }, +] + +[[package]] +name = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326 }, +] + +[[package]] +name = "rich" +version = "13.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/01/c954e134dc440ab5f96952fe52b4fdc64225530320a910473c1fe270d9aa/rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432", size = 221248 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/67/a37f6214d0e9fe57f6ae54b2956d550ca8365857f42a1ce0392bb21d9410/rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", size = 240681 }, +] + +[[package]] +name = "rsa" +version = "4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, +] + +[[package]] +name = "runs" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "xmod" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/6d/b9aace390f62db5d7d2c77eafce3d42774f27f1829d24fa9b6f598b3ef71/runs-1.2.2.tar.gz", hash = "sha256:9dc1815e2895cfb3a48317b173b9f1eac9ba5549b36a847b5cc60c3bf82ecef1", size = 5474 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/d6/17caf2e4af1dec288477a0cbbe4a96fbc9b8a28457dce3f1f452630ce216/runs-1.2.2-py3-none-any.whl", hash = "sha256:0980dcbc25aba1505f307ac4f0e9e92cbd0be2a15a1e983ee86c24c87b839dfd", size = 7033 }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252 }, +] + +[[package]] +name = "twine" +version = "5.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "keyring" }, + { name = "pkginfo" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/68/bd982e5e949ef8334e6f7dcf76ae40922a8750aa2e347291ae1477a4782b/twine-5.1.1.tar.gz", hash = "sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db", size = 225531 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/ec/00f9d5fd040ae29867355e559a94e9a8429225a0284a3f5f091a3878bfc0/twine-5.1.1-py3-none-any.whl", hash = "sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997", size = 38650 }, +] + +[[package]] +name = "urllib3" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/6d/fa469ae21497ddc8bc93e5877702dca7cb8f911e337aca7452b5724f1bb6/urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168", size = 292266 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", size = 121444 }, +] + +[[package]] +name = "warnet" +version = "0.10.0" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "docker" }, + { name = "flask" }, + { name = "inquirer" }, + { name = "kubernetes" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tabulate" }, +] + +[package.optional-dependencies] +build = [ + { name = "build" }, + { name = "twine" }, +] + +[package.metadata] +requires-dist = [ + { name = "build", marker = "extra == 'build'" }, + { name = "click", specifier = "==8.1.7" }, + { name = "docker", specifier = "==7.1.0" }, + { name = "flask", specifier = "==3.0.3" }, + { name = "inquirer", specifier = "==3.4.0" }, + { name = "kubernetes", specifier = "==30.1.0" }, + { name = "pyyaml", specifier = "==6.0.2" }, + { name = "rich", specifier = "==13.7.1" }, + { name = "tabulate", specifier = "==0.9.0" }, + { name = "twine", marker = "extra == 'build'" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 }, +] + +[[package]] +name = "werkzeug" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/e2/6dbcaab07560909ff8f654d3a2e5a60552d937c909455211b1b36d7101dc/werkzeug-3.0.4.tar.gz", hash = "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306", size = 803966 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/84/997bbf7c2bf2dc3f09565c6d0b4959fefe5355c18c4096cfd26d83e0785b/werkzeug-3.0.4-py3-none-any.whl", hash = "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c", size = 227554 }, +] + +[[package]] +name = "xmod" +version = "1.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/b2/e3edc608823348e628a919e1d7129e641997afadd946febdd704aecc5881/xmod-1.8.1.tar.gz", hash = "sha256:38c76486b9d672c546d57d8035df0beb7f4a9b088bc3fb2de5431ae821444377", size = 3988 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/0dc75b64a764ea1cb8e4c32d1fb273c147304d4e5483cd58be482dc62e45/xmod-1.8.1-py3-none-any.whl", hash = "sha256:a24e9458a4853489042522bdca9e50ee2eac5ab75c809a91150a8a7f40670d48", size = 4610 }, +] + +[[package]] +name = "zipp" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/af/9f2de5bd32549a1b705af7a7c054af3878816a1267cb389c03cc4f342a51/zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31", size = 23244 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/cc/b9958af9f9c86b51f846d8487440af495ecf19b16e426fce1ed0b0796175/zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d", size = 9432 }, +]