├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── ci-requirements.txt ├── requirements.txt ├── src └── plugin.py └── tests └── test_plugin.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build-and-test: 14 | name: Integration Tests 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | bitcoin-version: ["24.0.1", "23.1"] 19 | lightningd-version: ["master", "v23.02", "v22.11.1"] 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v3 24 | 25 | - name: Build Docker image 26 | run: | 27 | docker build --build-arg LIGHTNINGD_VERSION=${{ matrix.lightningd-version }} \ 28 | --build-arg BITCOIN_VERSION=${{ matrix.bitcoin-version }} \ 29 | -t cl-test . 30 | 31 | - name: Run Integration Tests 32 | run: docker run -v $(pwd):/build cl-test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pytest_cache 2 | __pycache__ 3 | .direnv 4 | .envrc 5 | .mypy_cache 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest as builder 2 | MAINTAINER Christian Decker 3 | 4 | ARG LIGHTNINGD_VERSION=master 5 | ENV DEBIAN_FRONTEND=noninteractiv 6 | 7 | RUN apt-get update -qq \ 8 | && apt-get install -y --no-install-recommends \ 9 | git build-essential autoconf automake build-essential git libtool libgmp-dev \ 10 | libsqlite3-dev python3 python3-mako net-tools zlib1g-dev libsodium-dev \ 11 | gettext apt-transport-https ca-certificates python3-pip wget 12 | 13 | RUN git clone --recursive https://github.com/ElementsProject/lightning.git /tmp/lightning 14 | WORKDIR /tmp/lightning 15 | RUN git checkout $LIGHTNINGD_VERSION 16 | RUN ./configure --prefix=/tmp/lightning_install --enable-developer --disable-valgrind --enable-experimental-features 17 | RUN make -j $(nproc) install 18 | 19 | FROM ubuntu:latest as final 20 | 21 | COPY --from=builder /tmp/lightning_install/ /usr/local/ 22 | COPY --from=builder /tmp/lightning/ /usr/local/src/lightning/ 23 | 24 | RUN apt-get update -qq \ 25 | && apt-get install -y --no-install-recommends \ 26 | libsqlite3-dev \ 27 | zlib1g-dev \ 28 | libsodium-dev \ 29 | libgmp-dev \ 30 | python3 \ 31 | python3-pip \ 32 | wget \ 33 | && rm -rf /var/lib/apt/lists/* 34 | 35 | ARG BITCOIN_VERSION=24.0.1 36 | ENV BITCOIN_TARBALL bitcoin-${BITCOIN_VERSION}-x86_64-linux-gnu.tar.gz 37 | ENV BITCOIN_URL https://bitcoincore.org/bin/bitcoin-core-$BITCOIN_VERSION/$BITCOIN_TARBALL 38 | 39 | RUN cd /tmp \ 40 | && wget -qO $BITCOIN_TARBALL "$BITCOIN_URL" \ 41 | && BD=bitcoin-$BITCOIN_VERSION/bin \ 42 | && tar -xzvf $BITCOIN_TARBALL $BD/bitcoin-cli $BD/bitcoind --strip-components=1 \ 43 | && cp bin/bitcoind bin/bitcoin-cli /usr/bin/ \ 44 | && rm -rf $BITCOIN_TARBALL bin 45 | 46 | # Make the debug logs available during testing 47 | ENV TEST_DEBUG 1 48 | 49 | # Speed up testing by shortening all timeouts 50 | ENV DEVELOPER 1 51 | 52 | WORKDIR /build 53 | ADD ci-requirements.txt /tmp/ 54 | 55 | RUN pip3 install /usr/local/src/lightning/contrib/pyln-client 56 | RUN pip3 install /usr/local/src/lightning/contrib/pyln-testing 57 | RUN pip3 install -r /tmp/ci-requirements.txt 58 | 59 | CMD ["pytest", "-vvv", "--timeout=600", "-n=4"] 60 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | docker-image: 4 | docker build -t cl-test . 5 | 6 | docker-test: 7 | docker run -ti -v $(shell pwd):/build cl-test 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # c-lightning Project Template (Updated for Bitcoin 24 and Default CI) 2 | 3 | Getting started with a new project can be a daunting task, especially on a 4 | complex project like a Lightning node. This template tries to get you going as 5 | fast as possible, by providing a couple of utilities and a preconfigured 6 | environment. The environment consists of a fully fledged python project 7 | including tests, so you can start on your implementation without having to 8 | learn all the obscure incantations first. 9 | 10 | ## Who is this template for? 11 | 12 | Everybody! While primarily created to get a number of academic projects going, 13 | it provides an easy to extend base that can be built upon: 14 | 15 | - Researchers trying to investigate the Lightning Network can create 16 | controlled network setups that highlight the aspects that they are looking 17 | into. 18 | - Plugin developers that want to extend c-lightning have a template of a 19 | plugin that can be incrementally changed to add new cool features. 20 | - Application developer that need a working test network they can use to test 21 | their applications against. 22 | 23 | ## Getting Started 24 | 25 | Out of the box this template provides a number of pieces that should get you 26 | started right away: 27 | 28 | - A sample plugin in `src/plugin.py` that shows you how easy it is to write a 29 | plugin to extend the existing functionality in c-lightning 30 | - Some tests that show how a network can be bootstrapped, and each node can 31 | be configured, with or without plugins. 32 | - A docker image that contains the required dependencies, so you can test and 33 | develop in isolation. 34 | - A `Makefile` that provides shortcuts to run tests and build artifacts. 35 | 36 | You can get started with this template by checking it out, and start hacking: 37 | 38 | ```bash 39 | $ git clone https://github.com/lightningd/template.git my-awesome-project 40 | $ cd my-awesome-project 41 | ``` 42 | 43 | Alternatively you can also download a snapshot and start with that: 44 | 45 | ```bash 46 | $ wget https://github.com/lightningd/template/archive/master.zip -O my-awesome-project.zip 47 | $ unzip my-awesome-project.zip 48 | $ cd my-awesome-project 49 | ``` 50 | 51 | The template comes with some canned tests to illustrate how you can create a 52 | network, perform some actions on it, test some things, and then tear down the 53 | network again after the test ran. These tests can be run either directly, or 54 | with the provided docker image, if you don't want to install the 55 | dependencies. The following builds the docker image: 56 | 57 | ```bash 58 | make docker-image 59 | ``` 60 | 61 | And the following runs the tests in a docker container: 62 | 63 | ```bash 64 | make docker-test 65 | ``` 66 | 67 | ## Where to go next? 68 | 69 | Once you have familiarized yourself with how your tests can be run it's time 70 | to dig deeper. The following resources should help you on your journey: 71 | 72 | - The c-lightning [Plugin API docs][plugin-docs] describe the communication 73 | between c-lightning and your plugin. 74 | - The [pyln-client docs][pyln-client-docs] describe the JSON-RPC API client 75 | (`LightningRpc`) and the `Plugin` API used to talk to c-lightning over the 76 | JSON-RPC or to implement a plugin. 🚧 These docs are still under 77 | construction 🚧 78 | 79 | 80 | [plugin-docs]: https://lightning.readthedocs.io/PLUGINS.html 81 | [pyln-client-docs]: https://pyln-client.readthedocs.io/en/pyln/api.html 82 | -------------------------------------------------------------------------------- /ci-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-timeout 3 | pytest-xdist 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyln-client 2 | pyln-testing 3 | pytest 4 | pytest-timeout 5 | pytest-xdist 6 | -------------------------------------------------------------------------------- /src/plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """This is a small demo plugin 3 | """ 4 | from pyln.client import Plugin 5 | import time 6 | 7 | 8 | plugin = Plugin() 9 | 10 | 11 | # Functions decorated with `@plugin.method()` or `@plugin.async_method()` are 12 | # registered with the JSON-RPC exposed by `lightningd` and relayed to the 13 | # plugin that registered them. The `hello` function below for example can be 14 | # called like this: 15 | # 16 | # ```bash 17 | # $ lightning-cli hello "my friend!" 18 | # Hello my friend! 19 | # ``` 20 | # 21 | # As you can see this RPC passthrough also allows using default values and the 22 | # returned object is converted into a JSON object and returned by the call 23 | # from the client. 24 | @plugin.method("hello") 25 | def hello(plugin, name="world"): 26 | """This is the documentation string for the hello-function. 27 | 28 | It gets reported as the description when registering the function 29 | as a method with `lightningd`. 30 | 31 | It will also be returned if you call `lightning-cli help hello`. 32 | 33 | """ 34 | greeting = plugin.get_option('greeting') 35 | s = '{} {}'.format(greeting, name) 36 | plugin.log(s) 37 | return s 38 | 39 | 40 | # The special function decorated with `@plugin.init()` is called by 41 | # `lightningd` once it has finished initializing, the options have been parsed 42 | # and the RPC is now available. To make interacting with the JSON-RPC easier 43 | # `plugin` has an automatically configured RPC-client under `plugin.rpc`, so 44 | # you could for example retrieve information about the node using the 45 | # following 46 | # 47 | # ```python3 48 | # info = plugin.rpc.getinfo() 49 | # print(info['id']) 50 | # ``` 51 | @plugin.init() 52 | def init(options, configuration, plugin, **kwargs): 53 | plugin.log("Plugin helloworld.py initialized") 54 | 55 | 56 | # TODO Document 57 | @plugin.subscribe("connect") 58 | def on_connect(plugin, id, address, **kwargs): 59 | plugin.log("Received connect event for peer {}".format(id)) 60 | 61 | 62 | @plugin.subscribe("disconnect") 63 | def on_disconnect(plugin, id, **kwargs): 64 | plugin.log("Received disconnect event for peer {}".format(id)) 65 | 66 | # TODO Document 67 | @plugin.hook("htlc_accepted") 68 | def on_htlc_accepted(onion, htlc, plugin, **kwargs): 69 | plugin.log('on_htlc_accepted called') 70 | time.sleep(20) 71 | return {'result': 'continue'} 72 | 73 | 74 | # Options registered before the call to `plugin.run()` will be exposed by 75 | # `lightningd` as if they were its own options. The option defined below 76 | # allows you to start `lightningd` like this: 77 | # 78 | # ```bash 79 | # $ lightningd --plugin=/path/to/plugin.py --greeting=Ciao 80 | # ``` 81 | # 82 | # And the option will be passed to the plugin, where it can be accessed using 83 | # `plugin.get_option()` like in the `hello()` function above. 84 | plugin.add_option('greeting', 'Hello', 'The greeting I should use.') 85 | 86 | 87 | # After setting everything up it's time to give the plugin control of the 88 | # script by calling `plugin.run()`. It takes control of stdin/stdout and 89 | # starts communicating with `lightningd`. 90 | plugin.run() 91 | -------------------------------------------------------------------------------- /tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | from pyln.testing.fixtures import * 2 | import logging 3 | import os 4 | import sys 5 | 6 | 7 | logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) 8 | test_path = os.path.dirname(__file__) 9 | plugin_path = os.path.join(test_path, '..', 'src', 'plugin.py') 10 | 11 | 12 | def test_plugin_start(node_factory): 13 | """Simply try to start a node with the plugin. 14 | """ 15 | l1 = node_factory.get_node(options={'plugin': plugin_path}) 16 | print(l1.rpc.getinfo()) 17 | 18 | 19 | def test_plugin_rpc_call(node_factory): 20 | """Test the `hello` RPC method exposed by the plugin. 21 | """ 22 | l1 = node_factory.get_node(options={'plugin': plugin_path}) 23 | 24 | # Test with the default parameter "world" 25 | r = l1.rpc.hello() 26 | assert(r == 'Hello world') 27 | 28 | # Test by overriding the default parameter 29 | r = l1.rpc.hello('my friend!') 30 | assert(r == 'Hello my friend!') 31 | 32 | 33 | def test_plugin_option(node_factory): 34 | """The plugin adds a command-line options, let's test it. 35 | """ 36 | # Notice that we skip the two dashes at the beginning: 37 | opts = { 38 | 'plugin': plugin_path, 39 | 'greeting': 'Ciao', 40 | } 41 | l1 = node_factory.get_node(options=opts) 42 | r = l1.rpc.hello() 43 | assert(r == 'Ciao world') 44 | 45 | 46 | def test_payment(node_factory): 47 | """Let's just perform a test payment, without plugins this time. 48 | 49 | `node_factory` has a `line_graph` helper function that creates the very 50 | common line network for testing: it creates `n` nodes, and connects each 51 | one with the next one with a channel. 52 | 53 | """ 54 | l1, l2 = node_factory.line_graph(2) 55 | 56 | # Create an invoice from node `l2` for `l1` to pay. Notice that label 57 | # needs to be unique since that's how you later look it up again. We only 58 | # care about the `bolt11`-encoded invoice. 59 | inv = l2.rpc.invoice(1337, "label", "description")['bolt11'] 60 | 61 | # Now just let `l1` pay the invoice: 62 | l1.rpc.pay(inv) 63 | 64 | # Now check with `l2` that it marked the invoice as paid: 65 | invoices = l2.rpc.listinvoices("label")['invoices'] 66 | 67 | # Since we provided a label we should only get one: 68 | assert(len(invoices) == 1) 69 | invoice = invoices[0] 70 | 71 | # Since l1 paid it, we should see it as paid: 72 | assert(invoice['status'] == 'paid') 73 | --------------------------------------------------------------------------------