├── .dockerignore
├── .flake8
├── .github
└── workflows
│ ├── build.yml
│ └── testing.yml
├── .gitignore
├── CHANGELOG.md
├── HACKING.md
├── LICENSE
├── Makefile
├── README.md
├── changelog.json
├── docker
├── Dockerfile.clightning
├── Dockerfile.ldk
├── docker-compose.yml
├── entrypoint.sh
└── ldk-entrypoint.sh
├── lnprototest
├── __init__.py
├── backend
│ ├── __init__.py
│ ├── backend.py
│ └── bitcoind.py
├── bitfield.py
├── clightning
│ ├── __init__.py
│ ├── clightning.py
│ └── requirements.txt
├── commit_tx.py
├── dummyrunner.py
├── errors.py
├── event.py
├── funding.py
├── keyset.py
├── namespace.py
├── proposals.py
├── runner.py
├── signature.py
├── stash
│ ├── __init__.py
│ └── stash.py
├── structure.py
└── utils
│ ├── __init__.py
│ ├── bitcoin_utils.py
│ ├── ln_spec_utils.py
│ └── utils.py
├── poetry.lock
├── pyproject.toml
├── tests
├── conftest.py
├── pytest.ini
├── test_bolt1-01-init.py
├── test_bolt1-02-unknown-messages.py
├── test_bolt2-01-close_channel.py
├── test_bolt2-01-open_channel.py
├── test_bolt2-02-reestablish.py
├── test_bolt2-10-add-htlc.py
├── test_bolt2-20-open_channel_accepter.py
├── test_bolt2-30-channel_type-open-accept-tlvs.py
├── test_bolt7-01-channel_announcement-success.py
├── test_bolt7-02-channel_announcement-failure.py
├── test_bolt7-10-gossip-filter.py
└── test_bolt7-20-query_channel_range.py
└── tools
└── check_quotes.py
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/__py*
2 | **/mypy_*
3 | **/*.venv*
4 | **/*.egg-info
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 88
3 | extend-ignore = E203
4 | per-file-ignores = __init__.py:F401
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build and check code style
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | python-version: [3.9, 3.11, 3.12, 3.13]
16 |
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Set up Python ${{ matrix.python-version }}
20 | uses: actions/setup-python@v2
21 | with:
22 | python-version: ${{ matrix.python-version }}
23 | - name: Install dependencies
24 | run: |
25 | python -m pip install --upgrade pip
26 | pip install poetry
27 | poetry config virtualenvs.create false
28 | poetry install
29 | - name: Code style check
30 | run: |
31 | black . --check --diff
32 | - name: Test with pytest
33 | run: |
34 | pytest tests
35 |
--------------------------------------------------------------------------------
/.github/workflows/testing.yml:
--------------------------------------------------------------------------------
1 | name: Integration testing
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 | schedule:
9 | - cron: '30 1 1,15 * *'
10 |
11 | jobs:
12 | test:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | fail-fast: false
16 | matrix:
17 | LN_IMPL: [clightning, ldk]
18 |
19 | steps:
20 | - uses: actions/checkout@v2
21 | - name: Integration testing
22 | run: |
23 | docker build -f docker/Dockerfile.${{matrix.LN_IMPL}} -t lnprototest-${{matrix.LN_IMPL}} .
24 | docker run -e LN_IMPL=${{matrix.LN_IMPL}} lnprototest-${{matrix.LN_IMPL}}
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | *~
3 | TAGS
4 | tags
5 | **/github-merge.py
6 | .venv
7 | **.egg-info
8 | .idea
9 | dist
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # v0.0.5
2 |
3 | ## Fixed
4 | - Correct reorg issue in the ln helper function ([commit](https://github.com/rustyrussell/lnprototest/commit/ad3129d4dad14e98a0b3f1210d349d2b6503ce53)). @vincenzopalazzo 21-09-2023
5 | - Correct error check for expected errors ([commit](https://github.com/rustyrussell/lnprototest/commit/3dafe1d0b010aa0fb98b545a3a0fef6985153087)). @vincenzopalazzo 15-07-2023
6 | - runner_features added in runner.py and applied in ln_spec_utils.py ([commit](https://github.com/rustyrussell/lnprototest/commit/0dc5dddbb209bd8edf4d2a93973b72882724b865)). @Psycho-Pirate 24-06-2023
7 | - Modified Reestablish test to use new code and used stash ([commit](https://github.com/rustyrussell/lnprototest/commit/6cd0791f961d7c8d596d45c9725a463e5f52eed4)). @Psycho-Pirate 19-06-2023
8 | - resolve the number callback in Block event ([commit](https://github.com/rustyrussell/lnprototest/commit/7ff8d9f33253031ff83bc9aed5ff1b63bd42bf95)). @vincenzopalazzo 06-06-2023
9 |
10 | ## Added
11 | - Allowing the option to skip the creation of the wallet in bitcoind ([commit](https://github.com/rustyrussell/lnprototest/commit/45711defc89161b8d3efcae747000ae4fb2fd36d)). @vincenzopalazzo 27-07-2023
12 | - Open channel helper ignores announcement signatures ([commit](https://github.com/rustyrussell/lnprototest/commit/ba32816e3291055e7f1eeff01f872f8a3359f66b)). @vincenzopalazzo 16-06-2023
13 | - Support the Drop of the --developer flag at runtime (cln: specify --developer if supported. rustyrussell/lnprototest#106). @rustyrussell
14 |
15 |
16 | # v0.0.4
17 |
18 | ## Fixed
19 | - Grab the feature from the init message if it is not specified ([commit](https://github.com/rustyrussell/lnprototest/commit/e2005d731bacf4fffaf4fe92cc96b1d241bde7f8)). @vincenzopalazzo 03-06-2023
20 | - pass the timeout when we are waiting a node to start ([commit](https://github.com/rustyrussell/lnprototest/commit/9b4a0c1521eddee3f1c90aae6bab1aac120c4cba)). @vincenzopalazzo 01-06-2023
21 | - import inside the library some useful utils ([commit](https://github.com/rustyrussell/lnprototest/commit/554659fbdce8376f9f200e98a05f44b3b5c0582c)). @vincenzopalazzo 01-06-2023
22 |
23 | ## Added
24 | - Enable stashing of expected messages ([commit](https://github.com/rustyrussell/lnprototest/commit/50d72b1f8b08c3d973c8252f3be3c28812247404)). @vincenzopalazzo 01-06-2023
25 |
--------------------------------------------------------------------------------
/HACKING.md:
--------------------------------------------------------------------------------
1 | # Adding New Tests, Testing New Nodes
2 |
3 | The most common thing to do is to add a new test for a new feature.
4 |
5 | ## Adding A New Test
6 |
7 | To add a new test, simply add a file starting with `test_` to the
8 | tests/ directory. Every function in this file starting with `test_`
9 | will be run (the rest, presumably, are helpers you need).
10 |
11 | For every test, there is a runner which wraps a particular node
12 | implementation: using the default "DummyRunner" helps debug the tests
13 | themselves.
14 |
15 | A test consists of one or more Events (e.g. send a message, receive a
16 | message), in a DAG. The test runner repeats the test until every
17 | Event has been covered. The most important event is probably
18 | TryAll(), which gives multiple alternative paths of Events, each of
19 | which should be tried (it will try the "most Events" path first, to
20 | try to get maximum coverage early in testing).
21 |
22 | Tests which don't have an ExpectError event have a check at the end to
23 | make sure no errors occurred.
24 |
25 | ## Using ExpectMsg Events
26 |
27 | `ExpectMsg` matches a (perhaps only partially defined) message, then
28 | calls its `if_match` function which can do more fine-grained matching.
29 | For example, it could check that a specific field is not specified, or
30 | a specific bit is set, etc. There's also `ignore` which is a list
31 | of Message to ignore: it defaults to common gossip queries.
32 |
33 | `ExpectMsg` also stores the received fields in the runner's `stash`:
34 | the convenient `rcvd` function can be used to access them for use in
35 | `Msg` fields.
36 |
37 |
38 | ## Creating New Event Types
39 |
40 | For various special effects, you might want to create a new Event
41 | subclass.
42 |
43 | Events are constructed once, but then their `action` method is called
44 | in multiple orders for multiple traverses: they can store state across
45 | runs in the `runner` using its `add_stash()` and `get_stash()`
46 | methods, as used by `ExpectMsg` and `Msg`. The entire stash
47 | is emptied upon restart.
48 |
49 |
50 | ## Test Checklist
51 |
52 | 1. Did you quote the part of the BOLT you are testing? This is vital
53 | to make your tests readable, and to ensure they change with the
54 | spec. `make check-quotes` will all the quotes (starting with `#
55 | BOLT #N:`) are correct based on the `../lightning-rfc` directory,
56 | or run `tools/check_quotes.py testfile`. If you are creating tests
57 | for a specific (e.g. non-master) git revision, you can use `#
58 | BOLT-commitid #N:` and use `--include-commit=commitid` option for
59 | every commit id it should check.
60 |
61 | 2. Does your test check failures as well as successes?
62 |
63 | 3. Did you test something which wasn't clear in the spec? Consider
64 | opening a PR or issue to add an explicit requirement.
65 |
66 | 4. Does it pass `make check-source` a.k.a. flake8 and mypy?
67 |
68 | ## Adding a New Runner
69 |
70 | You can write a new runner for an implementation by inheriting from
71 | the Runner class. This runner could live in this repository or in
72 | your implementation's repository: you can set it with
73 | `--runner=modname.classname`.
74 |
75 | This is harder than writing a new test, but ultimately far more
76 | useful, as it expands the coverage of every new test.
77 |
78 | To add a new runner, you'll need to create a new subclass of Runner, that
79 | fills in the Runner API. You can find a good skeleton for a new runner in
80 | `lnprototest/dummyrunner.py`
81 |
82 | A completed core-lightning example runner can be found in `lnprototest/clightning/clightning.py`
83 |
84 | Here's a short outline of the current expected methods for a Runner.
85 |
86 | - `get_keyset`: returns the node's KeySet (`revocation_base_secret`, `payment_base_secret`, `htlc_base_secret`, and `shachain_seed`)
87 | - `get_node_privkey`: Private key of the node. Used to generate the node id and establish a communication channel with the node under test.
88 | - `get_node_bitcoinkey`: Private key of the node under test's funding pubkey
89 | - `has_option`: checks for features (e.g. `option_anchor_outputs`) in which cast it returns `None`, or "even" or "odd" (required or supported). Also checks for non-feature-bit features, such as `supports_open_accept_channel_types` which returns `None` or "true".
90 | - `add_startup_flag`: Add flag to runner's startup.
91 | - `start`: Starts up / initializes the node under test.
92 | - `stop`: Stops the node under test and closes the connection.
93 | - `restart`: Restarts the node under tests, closes the existing connection, cleans up the existing test files, and restarts bitcoind. Note that it's useful to print a `RESTART` log when verbose logging is activated, e.g.
94 |
95 | if self.config.getoption('verbose'):
96 | print("[RESTART]")
97 |
98 | - `connect`: Create a connection to the node under test using the provided `connprivkey`.
99 | - `getblockheight`: Return the blockcount from bitcoind
100 | - `trim_blocks`: Invalidate bitcoind blocks until `newheight`
101 | - `add_blocks`: Send provided `txs` (if any). Generate `n` new blocks.
102 | - `disconnect`: Implemented in the parent Runner, not necessary to implement in child unless necessary.
103 | - `recv`: Send `outbuf` over `conn` to node under test
104 | - `fundchannel`: Initiate a fundchannel attempt to the connection's pubkey (the test harness) for the given `amount` and `feerate`. MUST NOT block (should execute this fundchannel request on a secondary thread)
105 | - `init_rbf`: For v2 channel opens, initiates an RBF attempt. Same as `fundchannel`, must not block.
106 | - `invoice`: Generate an invoice from the node under test for the given amount and preimage
107 | - `accept_add_fund`: Configure the node under test to contribute to any incoming v2 open channel offers.
108 | - `addhtlc`: Add the provided htlc to the the node. core lightning does this via the `sendpay` command
109 | - `get_output_message`: Read a message from the node's connection
110 | - `expect_tx`: Wait for the provided txid to appear in the mempool
111 | - `check_error`: Gets message from connection and returns it as hex. Also calls parent Runner method (which marks this as an `expected_error`)
112 | - `check_final_error`: Called by Runner.disconnect(). Closes the connection by forcing a disconnect on the peer. Processes all remaining messages from peer. Raises EventError if error message is returned.
113 |
114 |
115 | ### Passing cmdline args to the Runner
116 | Note that the core-lightning runner, in `__init__`, converts
117 | cmdline `runner_args` into a `startup_flag` array, which are then
118 | passed to the node at `start`
119 |
120 | Relevant portion from `clightning.py/Runner#__init__`
121 | ```
122 | self.startup_flags = []
123 | for flag in config.getoption("runner_args"):
124 | self.startup_flags.append("--{}".format(flag))
125 | ```
126 |
127 |
128 | Relevant portion from `clightning.py/Runner#start`
129 | ```
130 | self.proc = subprocess.Popen(['{}/lightningd/lightningd'.f...
131 | '--network=regtest',
132 | '--bitcoin-rpcuser=rpcuser',
133 | '--bitcoin-rpcpassword=rpcpass',
134 | '--bitcoin-rpcport={}'.format(self.bitcoind.port),
135 | '--log-level=debug',
136 | '--log-file=log']
137 | + self.startup_flags)
138 | ```
139 |
140 |
141 |
142 | ### Initializing the funding for the tests with `submitblock`
143 |
144 | Note that the bitcoind backend in `lnprototest/backend/bitcoind.py`
145 | creates an initial block spendable by the privkey
146 | `cUB4V7VCk6mX32981TWviQVLkj3pa2zBcXrjMZ9QwaZB5Kojhp59`, then an
147 | additional 100 blocks so it's mature. `tests/helpers.py` has
148 | `tx_spendable` which spends this into several useful outputs, and many
149 | tests rely on this.
150 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2024 Rusty Russell (Blockstream), Vincenzo Palazzo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | #! /usr/bin/make
2 |
3 | PYTHONFILES := $(shell find * ! -path "build/*" ! -path "venv/*" -name '*.py')
4 | POSSIBLE_PYTEST_NAMES=pytest-3 pytest3 pytest
5 | PYTEST := $(shell for p in $(POSSIBLE_PYTEST_NAMES); do if type $$p > /dev/null; then echo $$p; break; fi done)
6 | TEST_DIR=tests
7 |
8 | default: check-source check check-quotes
9 |
10 | check-pytest-found:
11 | @if [ -z "$(PYTEST)" ]; then echo "Cannot find any pytest: $(POSSIBLE_PYTEST_NAMES)" >&2; exit 1; fi
12 |
13 | check: check-pytest-found
14 | $(PYTEST) $(PYTEST_ARGS) $(TEST_DIR)
15 |
16 | check-source: check-fmt check-mypy check-internal-tests
17 |
18 | check-mypy:
19 | mypy --ignore-missing-imports --disallow-untyped-defs --disallow-incomplete-defs $(PYTHONFILES)
20 |
21 | check-internal-tests: check-pytest-found
22 | $(PYTEST) `find lnprototest -name '*.py'`
23 |
24 | check-quotes/%: %
25 | tools/check_quotes.py $*
26 |
27 | check-quotes: $(PYTHONFILES:%=check-quotes/%)
28 |
29 | check-fmt:
30 | black --check .
31 |
32 | fmt:
33 | black .
34 |
35 | TAGS:
36 | etags `find . -name '*.py'`
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
lnprototest
3 |
4 |
5 | a Testsuite for the Lightning Network Protocol
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | lnprototest is a set of test helpers written in Python3, designed to
23 | make it easy to write new tests when you propose changes to the
24 | lightning network protocol, as well as test existing implementations.
25 |
26 | ## Install requirements
27 |
28 | To install the necessary dependences
29 |
30 | ```bash
31 | pip3 install poetry
32 | poetry shell
33 | poetry install
34 | ```
35 |
36 | Well, now we can run the test
37 |
38 | ## Running test
39 |
40 | The simplest way to run is with the "dummy" runner:
41 |
42 | make check
43 |
44 | Here are some other useful pytest options:
45 |
46 | 1. `-n8` to run 8-way parallel.
47 | 2. `-x` to stop on the first failure.
48 | 3. `--pdb` to enter the debugger on first failure.
49 | 4. `--trace` to enter the debugger on every test.
50 | 5. `-k foo` to only run tests with 'foo' in their name.
51 | 6. `tests/test_bolt1-01-init.py` to only run tests in that file.
52 | 7. `tests/test_bolt1-01-init.py::test_init` to only run that test.
53 | 8. `--log-cli-level={LEVEL_NAME}` to enable the logging during the test execution.
54 |
55 | ### Running Against A Real Node.
56 |
57 | The more useful way to run is to use an existing implementation. So
58 | far, core-lightning is supported. You will need:
59 |
60 | 1. `bitcoind` installed, and in your path.
61 | 2. [`lightningd`](https://github.com/ElementsProject/lightning/) compiled with
62 | `--enable-developer`. By default the source directory should be
63 | `../lightning` relative to this directory, otherwise use
64 | `export LIGHTNING_SRC=dirname`.
65 | 3. Install any python requirements by
66 | `pip3 install -r lnprototest/clightning/requirements.txt`.
67 |
68 | Then you can run
69 |
70 | make check PYTEST_ARGS='--runner=lnprototest.clightning.Runner'
71 |
72 | or directly:
73 |
74 | pytest --runner=lnprototest.clightning.Runner
75 |
76 | # Further Work
77 |
78 | If you want to write new tests or new backends, see [HACKING.md](HACKING.md).
79 |
80 | Let's keep the sats flowing!
81 |
82 | Rusty.
83 |
--------------------------------------------------------------------------------
/changelog.json:
--------------------------------------------------------------------------------
1 | {
2 | "package_name": "lnprototest",
3 | "version": "v0.0.5",
4 | "api": {
5 | "name": "github",
6 | "repository": "rustyrussell/lnprototest",
7 | "branch": "master"
8 | },
9 | "generation_method": {
10 | "name": "semver-v2",
11 | "header_filter": false
12 | },
13 | "serialization_method": {
14 | "name": "md"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/docker/Dockerfile.clightning:
--------------------------------------------------------------------------------
1 | FROM ubuntu:22.04
2 | LABEL mantainer="Vincenzo Palazzo vincenzopalazzodev@gmail.com"
3 |
4 | WORKDIR /work
5 |
6 | ENV BITCOIN_VERSION=23.0
7 | ENV DEBIAN_FRONTEND=noninteractive
8 |
9 | RUN apt-get -qq update && \
10 | apt-get -qq install --no-install-recommends --allow-unauthenticated -yy \
11 | autoconf \
12 | automake \
13 | clang \
14 | cppcheck \
15 | docbook-xml \
16 | shellcheck \
17 | eatmydata \
18 | software-properties-common \
19 | build-essential \
20 | autoconf \
21 | locales \
22 | libtool \
23 | libprotobuf-c-dev \
24 | libsqlite3-dev \
25 | libgmp-dev \
26 | git \
27 | python3 \
28 | valgrind \
29 | net-tools \
30 | python3-mako \
31 | python3-pip \
32 | python3-setuptools \
33 | python-pkg-resources \
34 | python3-dev \
35 | virtualenv \
36 | shellcheck \
37 | libxml2-utils \
38 | wget \
39 | gettext \
40 | xsltproc \
41 | zlib1g-dev \
42 | jq && \
43 | rm -rf /var/lib/apt/lists/*
44 |
45 | ENV LANGUAGE=en_US.UTF-8
46 | ENV LANG=en_US.UTF-8
47 | ENV LC_ALL=en_US.UTF-8
48 | RUN locale-gen en_US.UTF-8 && dpkg-reconfigure locales
49 |
50 | RUN cd /tmp/ && \
51 | wget https://bitcoincore.org/bin/bitcoin-core-$BITCOIN_VERSION/bitcoin-$BITCOIN_VERSION-x86_64-linux-gnu.tar.gz -O bitcoin.tar.gz && \
52 | tar -xvzf bitcoin.tar.gz && \
53 | mv /tmp/bitcoin-$BITCOIN_VERSION/bin/bitcoin* /usr/local/bin/ && \
54 | rm -rf bitcoin.tar.gz /tmp/bitcoin-$BITCOIN_VERSION
55 |
56 | RUN pip3 install -U pip && \
57 | pip3 install -U poetry
58 |
59 | RUN git config --global user.name "John Doe" && \
60 | git config --global user.email johndoe@example.com && \
61 | git clone https://github.com/ElementsProject/lightning.git && \
62 | # FIXME: cln 24.05 is the last version that works with the current lnprototest.
63 | cd lightning && git checkout v24.05 && \
64 | pip3 install mako --break-system-packages && pip3 install grpcio-tools --break-system-packages && \
65 | ./configure && \
66 | make -j$(nproc)
67 |
68 | RUN mkdir lnprototest
69 |
70 | COPY . lnprototest
71 |
72 | RUN cd lnprototest && \
73 | poetry config virtualenvs.create false && \
74 | poetry install
75 | RUN cd lnprototest && ls -lha
76 |
77 | CMD ["./lnprototest/docker/entrypoint.sh"]
78 |
--------------------------------------------------------------------------------
/docker/Dockerfile.ldk:
--------------------------------------------------------------------------------
1 | FROM ubuntu:22.04
2 | LABEL maintainer="Prakhar Saxena prakharrsaxena@gmail.com"
3 |
4 | ENV TZ=Europe/Minsk
5 | ENV BITCOIN_VERSION=23.0
6 |
7 | # Set timezone
8 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
9 |
10 | # Install Ubuntu utilities and essential dependencies
11 | RUN apt-get update && apt-get install -y \
12 | software-properties-common \
13 | build-essential \
14 | curl wget jq \
15 | git python3 python3-pip
16 |
17 | # Install Rust
18 | RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
19 | ENV PATH="/root/.cargo/bin:${PATH}"
20 | RUN echo 'source $HOME/.cargo/env' >> $HOME/.bashrc
21 |
22 | # Install Bitcoin Core
23 | RUN cd /tmp/ && \
24 | wget https://bitcoincore.org/bin/bitcoin-core-$BITCOIN_VERSION/bitcoin-$BITCOIN_VERSION-x86_64-linux-gnu.tar.gz -O bitcoin.tar.gz && \
25 | tar -xvzf bitcoin.tar.gz && \
26 | mv /tmp/bitcoin-$BITCOIN_VERSION/bin/bitcoin* /usr/local/bin/ && \
27 | rm -rf bitcoin.tar.gz /tmp/bitcoin-$BITCOIN_VERSION
28 |
29 | # Clone and build LDK-Sample
30 | RUN git clone https://github.com/Psycho-Pirate/ldk-sample.git
31 | RUN cd ldk-sample && cargo build
32 |
33 | # Install Poetry
34 | RUN pip install poetry
35 |
36 | # Set PYTHONPATH environment variable
37 | ENV LDK_SRC="/ldk-sample"
38 |
39 | # Set workdir and copy project files
40 | RUN mkdir lnprototest
41 | COPY . lnprototest
42 |
43 | RUN cd lnprototest && \
44 | poetry install && \
45 | poetry run pip install ldk-lnprototest
46 | RUN cd lnprototest && ls -lha
47 | RUN chmod +x ./lnprototest/docker/ldk-entrypoint.sh
48 |
49 | CMD ["./lnprototest/docker/ldk-entrypoint.sh"]
50 |
--------------------------------------------------------------------------------
/docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | services:
4 | lightningd:
5 | build:
6 | context: ../
7 | dockerfile: ./docker/Dockerfile.${LN_IMPL}
--------------------------------------------------------------------------------
/docker/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | cd lnprototest || exit
3 |
4 | for i in range{0..5};
5 | do
6 | if make check PYTEST_ARGS='--runner=lnprototest.clightning.Runner -n8 --dist=loadfile --log-cli-level=DEBUG'; then
7 | echo "iteration $i succeeded"
8 | else
9 | echo "iteration $i failed"
10 | exit 1
11 | fi
12 | done
--------------------------------------------------------------------------------
/docker/ldk-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | cd lnprototest || exit
3 |
4 | for i in range{0..5};
5 | do
6 | if poetry run make check PYTEST_ARGS="--runner=ldk_lnprototest.Runner -n8 --dist=loadfile --log-cli-level=DEBUG"; then
7 | echo "iteration $i succeeded"
8 | else
9 | echo "iteration $i failed"
10 | exit 1
11 | fi
12 | done
13 |
--------------------------------------------------------------------------------
/lnprototest/__init__.py:
--------------------------------------------------------------------------------
1 | """lnprototest: a framework and test suite for checking lightning spec protocol compliance.
2 |
3 | This package is unusual, in that its main purpose is to carry the unit
4 | tests, which can be run against a Lightning node implementation, using
5 | an adapter called a 'Runner'. Two runners are included: the
6 | DummyRunner which is the default, and mainly useful to sanity check
7 | the tests themselves, and clightning.Runner.
8 |
9 | The documentation for the classes themselves should cover much of the
10 | reference material, and the tutorial should get you started.
11 |
12 | """
13 |
14 | from .errors import EventError, SpecFileError
15 |
16 | from .event import (
17 | Event,
18 | Connect,
19 | Disconnect,
20 | Msg,
21 | RawMsg,
22 | ExpectMsg,
23 | MustNotMsg,
24 | Block,
25 | ExpectTx,
26 | FundChannel,
27 | InitRbf,
28 | Invoice,
29 | AddHtlc,
30 | CheckEq,
31 | ExpectError,
32 | ResolvableInt,
33 | ResolvableStr,
34 | Resolvable,
35 | ResolvableBool,
36 | msat,
37 | negotiated,
38 | DualFundAccept,
39 | Wait,
40 | CloseChannel,
41 | ExpectDisconnect,
42 | )
43 |
44 | from .structure import Sequence, OneOf, AnyOrder, TryAll
45 |
46 | from .runner import (
47 | Runner,
48 | Conn,
49 | remote_revocation_basepoint,
50 | remote_payment_basepoint,
51 | remote_delayed_payment_basepoint,
52 | remote_htlc_basepoint,
53 | remote_per_commitment_point,
54 | remote_per_commitment_secret,
55 | remote_funding_pubkey,
56 | remote_funding_privkey,
57 | )
58 | from .dummyrunner import DummyRunner
59 | from .namespace import (
60 | peer_message_namespace,
61 | namespace,
62 | assign_namespace,
63 | make_namespace,
64 | )
65 | from .bitfield import bitfield, has_bit, bitfield_len
66 | from .signature import SigType, Sig
67 | from .keyset import KeySet
68 | from .commit_tx import Commit, HTLC, UpdateCommit
69 | from .utils import (
70 | Side,
71 | privkey_expand,
72 | wait_for,
73 | LightningUtils,
74 | ScriptType,
75 | BitcoinUtils,
76 | )
77 | from .funding import (
78 | AcceptFunding,
79 | CreateFunding,
80 | CreateDualFunding,
81 | Funding,
82 | AddInput,
83 | AddOutput,
84 | FinalizeFunding,
85 | AddWitnesses,
86 | )
87 | from .proposals import dual_fund_csv, channel_type_csv
88 |
89 | __all__ = [
90 | "EventError",
91 | "SpecFileError",
92 | "Resolvable",
93 | "ResolvableInt",
94 | "ResolvableStr",
95 | "ResolvableBool",
96 | "Event",
97 | "Connect",
98 | "Disconnect",
99 | "DualFundAccept",
100 | "CreateDualFunding",
101 | "AddInput",
102 | "AddOutput",
103 | "FinalizeFunding",
104 | "AddWitnesses",
105 | "Msg",
106 | "RawMsg",
107 | "ExpectMsg",
108 | "Block",
109 | "ExpectTx",
110 | "FundChannel",
111 | "InitRbf",
112 | "Invoice",
113 | "AddHtlc",
114 | "ExpectError",
115 | "Sequence",
116 | "OneOf",
117 | "AnyOrder",
118 | "TryAll",
119 | "CheckEq",
120 | "MustNotMsg",
121 | "SigType",
122 | "Sig",
123 | "DummyRunner",
124 | "Runner",
125 | "Conn",
126 | "KeySet",
127 | "peer_message_namespace",
128 | "namespace",
129 | "assign_namespace",
130 | "make_namespace",
131 | "bitfield",
132 | "has_bit",
133 | "bitfield_len",
134 | "msat",
135 | "negotiated",
136 | "remote_revocation_basepoint",
137 | "remote_payment_basepoint",
138 | "remote_delayed_payment_basepoint",
139 | "remote_htlc_basepoint",
140 | "remote_per_commitment_point",
141 | "remote_per_commitment_secret",
142 | "remote_funding_pubkey",
143 | "remote_funding_privkey",
144 | "Commit",
145 | "HTLC",
146 | "UpdateCommit",
147 | "Side",
148 | "AcceptFunding",
149 | "CreateFunding",
150 | "Funding",
151 | "regtest_hash",
152 | "privkey_expand",
153 | "Wait",
154 | "dual_fund_csv",
155 | "channel_type_csv",
156 | "wait_for",
157 | "CloseChannel",
158 | "ExpectDisconnect",
159 | ]
160 |
--------------------------------------------------------------------------------
/lnprototest/backend/__init__.py:
--------------------------------------------------------------------------------
1 | from .backend import Backend
2 | from .bitcoind import Bitcoind
3 |
4 | __all__ = ["Backend", "Bitcoind"]
5 |
--------------------------------------------------------------------------------
/lnprototest/backend/backend.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/python3
2 | # This script exercises the backend implementation
3 |
4 | # Released by Rusty Russell under CC0:
5 | # https://creativecommons.org/publicdomain/zero/1.0/
6 | from abc import ABC, abstractmethod
7 |
8 |
9 | class Backend(ABC):
10 | """
11 | Generic implementation of Bitcoin backend
12 | This is useful when the LN node uses different type
13 | of bitcoin backend.
14 | """
15 |
16 | @abstractmethod
17 | def start(self) -> None:
18 | pass
19 |
20 | @abstractmethod
21 | def stop(self) -> None:
22 | pass
23 |
24 | @abstractmethod
25 | def restart(self) -> None:
26 | pass
27 |
--------------------------------------------------------------------------------
/lnprototest/backend/bitcoind.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # This script exercises the core-lightning implementation
3 |
4 | # Released by Rusty Russell under CC0:
5 | # https://creativecommons.org/publicdomain/zero/1.0/
6 |
7 | import os
8 | import shutil
9 | import subprocess
10 | import logging
11 | import socket
12 |
13 | from contextlib import closing
14 | from typing import Any, Callable, Optional
15 | from bitcoin.rpc import RawProxy
16 | from .backend import Backend
17 |
18 |
19 | class BitcoinProxy:
20 | """Wrapper for BitcoinProxy to reconnect.
21 |
22 | Long wait times between calls to the Bitcoin RPC could result in
23 | `bitcoind` closing the connection, so here we just create
24 | throwaway connections. This is easier than to reach into the RPC
25 | library to close, reopen and reauth upon failure.
26 | """
27 |
28 | def __init__(self, btc_conf_file: str, *args: Any, **kwargs: Any):
29 | self.btc_conf_file = btc_conf_file
30 |
31 | def __getattr__(self, name: str) -> Callable:
32 | if name.startswith("__") and name.endswith("__"):
33 | # Python internal stuff
34 | raise AttributeError
35 |
36 | def f(*args: Any) -> Callable:
37 | self.__proxy = RawProxy(btc_conf_file=self.btc_conf_file)
38 |
39 | logging.debug(
40 | "Calling {name} with arguments {args}".format(name=name, args=args)
41 | )
42 | res = self.__proxy._call(name, *args)
43 | logging.debug("Result for {name} call: {res}".format(name=name, res=res))
44 | return res
45 |
46 | # Make debuggers show rather than >
48 | f.__name__ = name
49 | return f
50 |
51 |
52 | class Bitcoind(Backend):
53 | """Starts regtest bitcoind on an ephemeral port, and returns the RPC proxy"""
54 |
55 | def __init__(self, basedir: str, with_wallet: Optional[str] = None):
56 | self.with_wallet = with_wallet
57 | self.rpc = None
58 | self.proc = None
59 | self.base_dir = basedir
60 | logging.debug(f"Base dir is {basedir}")
61 | self.bitcoin_dir = os.path.join(basedir, "bitcoind")
62 | self.bitcoin_conf = os.path.join(self.bitcoin_dir, "bitcoin.conf")
63 | self.cmd_line = [
64 | "bitcoind",
65 | "-datadir={}".format(self.bitcoin_dir),
66 | "-server",
67 | "-regtest",
68 | "-logtimestamps",
69 | "-nolisten",
70 | ]
71 | self.btc_version = None
72 |
73 | def __reserve(self) -> int:
74 | """
75 | When python asks for a free port from the os, it is possible that
76 | with concurrent access, the port that is picked is a port that is not free
77 | anymore when we go to bind the daemon like bitcoind port.
78 |
79 | Source: https://stackoverflow.com/questions/1365265/on-localhost-how-do-i-pick-a-free-port-number
80 | """
81 | with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
82 | s.bind(("", 0))
83 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
84 | return s.getsockname()[1]
85 |
86 | def __init_bitcoin_conf(self):
87 | """Init the bitcoin core directory with all the necessary information
88 | to startup the node"""
89 | if not os.path.exists(self.bitcoin_dir):
90 | os.makedirs(self.bitcoin_dir)
91 | logging.debug(f"Creating {self.bitcoin_dir} directory")
92 | self.port = self.__reserve()
93 | logging.debug("Port is {}, dir is {}".format(self.port, self.bitcoin_dir))
94 | # For after 0.16.1 (eg. 3f398d7a17f136cd4a67998406ca41a124ae2966), this
95 | # needs its own [regtest] section.
96 | logging.debug(f"Writing bitcoin conf file at {self.bitcoin_conf}")
97 | with open(self.bitcoin_conf, "w") as f:
98 | f.write("regtest=1\n")
99 | f.write("rpcuser=rpcuser\n")
100 | f.write("rpcpassword=rpcpass\n")
101 | f.write("[regtest]\n")
102 | f.write("rpcport={}\n".format(self.port))
103 | self.rpc = BitcoinProxy(btc_conf_file=self.bitcoin_conf)
104 |
105 | def __version_compatibility(self) -> None:
106 | """
107 | This method tries to manage the compatibility between
108 | different versions of Bitcoin Core implementation.
109 |
110 | This method could sometimes be useful when it is necessary to
111 | run the test with a different version of Bitcoin core.
112 | """
113 | if self.rpc is None:
114 | # Sanity check
115 | raise ValueError("bitcoind not initialized")
116 |
117 | self.btc_version = self.rpc.getnetworkinfo()["version"]
118 | assert self.btc_version is not None
119 | logging.debug("Bitcoin Core version {}".format(self.btc_version))
120 | if self.btc_version >= 210000:
121 | # Maintains the compatibility between wallet
122 | # different ln implementation can use the main wallet (?)
123 | self.rpc.createwallet(
124 | "main" if self.with_wallet is None else self.with_wallet
125 | ) # Automatically loads
126 |
127 | def __is__bitcoind_ready(self) -> bool:
128 | """Check if bitcoind is ready during the execution"""
129 | if self.proc is None:
130 | # Sanity check
131 | raise ValueError("bitcoind not initialized")
132 |
133 | # Wait for it to startup.
134 | while b"Done loading" not in self.proc.stdout.readline():
135 | pass
136 | return True
137 |
138 | def start(self) -> None:
139 | if self.rpc is None:
140 | self.__init_bitcoin_conf()
141 | # TODO: We can move this to a single call and not use Popen
142 | self.proc = subprocess.Popen(self.cmd_line, stdout=subprocess.PIPE)
143 | assert self.proc.stdout
144 |
145 | # Wait for it to startup.
146 | while not self.__is__bitcoind_ready():
147 | logging.debug("Bitcoin core is loading")
148 |
149 | self.__version_compatibility()
150 | # Block #1.
151 | # Privkey the coinbase spends to:
152 | # cUB4V7VCk6mX32981TWviQVLkj3pa2zBcXrjMZ9QwaZB5Kojhp59
153 | self.rpc.submitblock(
154 | "0000002006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f84591a56720aabc8023cecf71801c5e0f9d049d0c550ab42412ad12a67d89f3a3dbb6c60ffff7f200400000001020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff03510101ffffffff0200f2052a0100000016001419f5016f07fe815f611df3a2a0802dbd74e634c40000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000"
155 | )
156 | self.rpc.generatetoaddress(100, self.rpc.getnewaddress())
157 |
158 | def stop(self) -> None:
159 | self.rpc.stop()
160 | self.proc.kill()
161 | shutil.rmtree(os.path.join(self.bitcoin_dir, "regtest"))
162 |
163 | def restart(self) -> None:
164 | # Only restart if we have to.
165 | if self.rpc.getblockcount() != 101 or self.rpc.getrawmempool() != []:
166 | self.stop()
167 | self.start()
168 |
--------------------------------------------------------------------------------
/lnprototest/bitfield.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/python3
2 | from typing import Union, List
3 |
4 |
5 | def bitfield_len(bitfield: Union[List[int], str]) -> int:
6 | """Return length of this field in bits (assuming it's a bitfield!)"""
7 | if isinstance(bitfield, str):
8 | return len(bytes.fromhex(bitfield)) * 8
9 | else:
10 | return len(bitfield) * 8
11 |
12 |
13 | def has_bit(bitfield: Union[List[int], str], bitnum: int) -> bool:
14 | """Test bit in this bitfield (little-endian, as per BOLTs)"""
15 | bitlen = bitfield_len(bitfield)
16 | if bitnum >= bitlen:
17 | return False
18 |
19 | # internal to a msg, it's a list of int.
20 | if isinstance(bitfield, str):
21 | byte = bytes.fromhex(bitfield)[bitlen // 8 - 1 - bitnum // 8]
22 | else:
23 | byte = bitfield[bitlen // 8 - 1 - bitnum // 8]
24 |
25 | if (byte & (1 << (bitnum % 8))) != 0:
26 | return True
27 | else:
28 | return False
29 |
30 |
31 | def bitfield(*args: int) -> str:
32 | """Create a bitfield hex value with these bit numbers set"""
33 | bytelen = (max(args) + 8) // 8
34 | bfield = bytearray(bytelen)
35 | for bitnum in args:
36 | bfield[bytelen - 1 - bitnum // 8] |= 1 << (bitnum % 8)
37 | return bfield.hex()
38 |
--------------------------------------------------------------------------------
/lnprototest/clightning/__init__.py:
--------------------------------------------------------------------------------
1 | """Runner for the core-lightning implementation.
2 |
3 | This is adapted from the older testcases, so it is overly prescriptive
4 | of how the node is configured. A more modern implementation would simply
5 | reach into the node and derive the private keys it's using, rather than
6 | use hacky --dev options to override it.
7 |
8 | We could also factor out the bitcoind implementation, which is really
9 | independent.
10 |
11 | Important environment variables include TIMEOUT which sets how long we
12 | wait for responses (default, 30 seconds), and LIGHTNING_SRC which indicates
13 | where the binaries are (default ../lightning).
14 |
15 | """
16 |
17 | from .clightning import Runner
18 |
19 | __all__ = ["Runner"]
20 |
--------------------------------------------------------------------------------
/lnprototest/clightning/requirements.txt:
--------------------------------------------------------------------------------
1 | pyln-client
2 |
--------------------------------------------------------------------------------
/lnprototest/dummyrunner.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/python3
2 | # #### Dummy runner which you should replace with real one. ####
3 | import io
4 | from .runner import Runner, Conn
5 | from .event import Event, ExpectMsg, MustNotMsg
6 | from typing import List, Optional
7 | from .keyset import KeySet
8 | from pyln.proto.message import (
9 | Message,
10 | FieldType,
11 | DynamicArrayType,
12 | EllipsisArrayType,
13 | SizedArrayType,
14 | )
15 | from typing import Any
16 |
17 |
18 | class DummyRunner(Runner):
19 | def __init__(self, config: Any):
20 | super().__init__(config)
21 |
22 | def _is_dummy(self) -> bool:
23 | """The DummyRunner returns True here, as it can't do some things"""
24 | return True
25 |
26 | def get_keyset(self) -> KeySet:
27 | return KeySet(
28 | revocation_base_secret="11",
29 | payment_base_secret="12",
30 | htlc_base_secret="14",
31 | delayed_payment_base_secret="13",
32 | shachain_seed="FF" * 32,
33 | )
34 |
35 | def add_startup_flag(self, flag: str) -> None:
36 | if self.config.getoption("verbose"):
37 | print("[ADD STARTUP FLAG {}]".format(flag))
38 | return
39 |
40 | def get_node_privkey(self) -> str:
41 | return "01"
42 |
43 | def get_node_bitcoinkey(self) -> str:
44 | return "10"
45 |
46 | def has_option(self, optname: str) -> Optional[str]:
47 | return None
48 |
49 | def start(self) -> None:
50 | self.blockheight = 102
51 |
52 | def stop(self, print_logs: bool = False) -> None:
53 | pass
54 |
55 | def restart(self) -> None:
56 | super().restart()
57 | if self.config.getoption("verbose"):
58 | print("[RESTART]")
59 | self.blockheight = 102
60 |
61 | def connect(self, event: Event, connprivkey: str) -> None:
62 | if self.config.getoption("verbose"):
63 | print("[CONNECT {} {}]".format(event, connprivkey))
64 | self.add_conn(Conn(connprivkey))
65 |
66 | def getblockheight(self) -> int:
67 | return self.blockheight
68 |
69 | def trim_blocks(self, newheight: int) -> None:
70 | if self.config.getoption("verbose"):
71 | print("[TRIMBLOCK TO HEIGHT {}]".format(newheight))
72 | self.blockheight = newheight
73 |
74 | def add_blocks(self, event: Event, txs: List[str], n: int) -> None:
75 | if self.config.getoption("verbose"):
76 | print("[ADDBLOCKS {} WITH {} TXS]".format(n, len(txs)))
77 | self.blockheight += n
78 |
79 | def disconnect(self, event: Event, conn: Conn) -> None:
80 | super().disconnect(event, conn)
81 | if self.config.getoption("verbose"):
82 | print("[DISCONNECT {}]".format(conn))
83 |
84 | def recv(self, event: Event, conn: Conn, outbuf: bytes) -> None:
85 | if self.config.getoption("verbose"):
86 | print("[RECV {} {}]".format(event, outbuf.hex()))
87 |
88 | def fundchannel(
89 | self,
90 | event: Event,
91 | conn: Conn,
92 | amount: int,
93 | feerate: int = 253,
94 | expect_fail: bool = False,
95 | ) -> None:
96 | if self.config.getoption("verbose"):
97 | print(
98 | "[FUNDCHANNEL TO {} for {} at feerate {}. Expect fail? {}]".format(
99 | conn, amount, feerate, expect_fail
100 | )
101 | )
102 |
103 | def init_rbf(
104 | self,
105 | event: Event,
106 | conn: Conn,
107 | channel_id: str,
108 | amount: int,
109 | utxo_txid: str,
110 | utxo_outnum: int,
111 | feerate: int,
112 | ) -> None:
113 | if self.config.getoption("verbose"):
114 | print(
115 | "[INIT_RBF TO {} (channel {}) for {} at feerate {}. {}:{}".format(
116 | conn, channel_id, amount, feerate, utxo_txid, utxo_outnum
117 | )
118 | )
119 |
120 | def invoice(self, event: Event, amount: int, preimage: str) -> None:
121 | if self.config.getoption("verbose"):
122 | print("[INVOICE for {} with PREIMAGE {}]".format(amount, preimage))
123 |
124 | def accept_add_fund(self, event: Event) -> None:
125 | if self.config.getoption("verbose"):
126 | print("[ACCEPT_ADD_FUND]")
127 |
128 | def addhtlc(self, event: Event, conn: Conn, amount: int, preimage: str) -> None:
129 | if self.config.getoption("verbose"):
130 | print(
131 | "[ADDHTLC TO {} for {} with PREIMAGE {}]".format(conn, amount, preimage)
132 | )
133 |
134 | @staticmethod
135 | def fake_field(ftype: FieldType) -> str:
136 | if isinstance(ftype, DynamicArrayType) or isinstance(ftype, EllipsisArrayType):
137 | # Byte arrays are literal hex strings
138 | if ftype.elemtype.name == "byte":
139 | return ""
140 | return "[]"
141 | elif isinstance(ftype, SizedArrayType):
142 | # Byte arrays are literal hex strings
143 | if ftype.elemtype.name == "byte":
144 | return "00" * ftype.arraysize
145 | return (
146 | "["
147 | + ",".join([DummyRunner.fake_field(ftype.elemtype)] * ftype.arraysize)
148 | + "]"
149 | )
150 | elif ftype.name in (
151 | "byte",
152 | "u8",
153 | "u16",
154 | "u32",
155 | "u64",
156 | "tu16",
157 | "tu32",
158 | "tu64",
159 | "bigsize",
160 | "varint",
161 | ):
162 | return "0"
163 | elif ftype.name in ("chain_hash", "channel_id", "sha256"):
164 | return "00" * 32
165 | elif ftype.name == "point":
166 | return "038f1573b4238a986470d250ce87c7a91257b6ba3baf2a0b14380c4e1e532c209d"
167 | elif ftype.name == "short_channel_id":
168 | return "0x0x0"
169 | elif ftype.name == "signature":
170 | return "01" * 64
171 | else:
172 | raise NotImplementedError(
173 | "don't know how to fake {} type!".format(ftype.name)
174 | )
175 |
176 | def get_output_message(self, conn: Conn, event: ExpectMsg) -> Optional[bytes]:
177 | if self.config.getoption("verbose"):
178 | print("[GET_OUTPUT_MESSAGE {}]".format(conn))
179 |
180 | # We make the message they were expecting.
181 | msg = Message(event.msgtype, **event.resolve_args(self, event.kwargs))
182 |
183 | # Fake up the other fields.
184 | for m in msg.missing_fields():
185 | ftype = msg.messagetype.find_field(m.name)
186 | msg.set_field(m.name, self.fake_field(ftype.fieldtype))
187 |
188 | binmsg = io.BytesIO()
189 | msg.write(binmsg)
190 | return binmsg.getvalue()
191 |
192 | def expect_tx(self, event: Event, txid: str) -> None:
193 | if self.config.getoption("verbose"):
194 | print("[EXPECT-TX {}]".format(txid))
195 |
196 | def check_error(self, event: Event, conn: Conn) -> Optional[str]:
197 | super().check_error(event, conn)
198 | if self.config.getoption("verbose"):
199 | print("[CHECK-ERROR {}]".format(event))
200 | return "Dummy error"
201 |
202 | def check_final_error(
203 | self,
204 | event: Event,
205 | conn: Conn,
206 | expected: bool,
207 | must_not_events: List[MustNotMsg],
208 | ) -> None:
209 | pass
210 |
211 | def close_channel(self, channel_id: str) -> bool:
212 | if self.config.getoption("verbose"):
213 | print("[CLOSE-CHANNEL {}]".format(channel_id))
214 | return True
215 |
216 | def is_running(self) -> bool:
217 | return True
218 |
219 | def teardown(self):
220 | pass
221 |
--------------------------------------------------------------------------------
/lnprototest/errors.py:
--------------------------------------------------------------------------------
1 | from typing import TYPE_CHECKING
2 |
3 | if TYPE_CHECKING:
4 | from .event import Event
5 |
6 |
7 | class EventError(Exception):
8 | """Error thrown when the runner fails in some way"""
9 |
10 | def __init__(self, event: "Event", message: str):
11 | self.eventpath = [event]
12 | self.message = message
13 |
14 | def add_path(self, event: "Event") -> None:
15 | self.eventpath = [event] + self.eventpath
16 |
17 | def path_to_str(self) -> str:
18 | result = "["
19 | for event in self.eventpath:
20 | result += f"{event},"
21 | return f"{result}]"
22 |
23 | def __str__(self) -> str:
24 | return f"`{self.message}` on event {self.path_to_str()}"
25 |
26 |
27 | class SpecFileError(Exception):
28 | """Error thrown when the specification file has an error"""
29 |
30 | def __init__(self, event: "Event", message: str):
31 | self.event = event
32 | self.message = message
33 |
--------------------------------------------------------------------------------
/lnprototest/keyset.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/python3
2 | # FIXME: clean this up for use as pyln.proto.tx
3 | import coincurve
4 | import hashlib
5 |
6 |
7 | class KeySet(object):
8 | def __init__(
9 | self,
10 | revocation_base_secret: str,
11 | payment_base_secret: str,
12 | htlc_base_secret: str,
13 | delayed_payment_base_secret: str,
14 | shachain_seed: str,
15 | ):
16 | from .utils import privkey_expand, check_hex
17 |
18 | self.revocation_base_secret = privkey_expand(revocation_base_secret)
19 | self.payment_base_secret = privkey_expand(payment_base_secret)
20 | self.htlc_base_secret = privkey_expand(htlc_base_secret)
21 | self.delayed_payment_base_secret = privkey_expand(delayed_payment_base_secret)
22 | self.shachain_seed = bytes.fromhex(check_hex(shachain_seed, 64))
23 |
24 | def raw_payment_basepoint(self) -> coincurve.PublicKey:
25 | return coincurve.PublicKey.from_secret(self.payment_base_secret.secret)
26 |
27 | def payment_basepoint(self) -> str:
28 | return self.raw_payment_basepoint().format().hex()
29 |
30 | def raw_revocation_basepoint(self) -> coincurve.PublicKey:
31 | return coincurve.PublicKey.from_secret(self.revocation_base_secret.secret)
32 |
33 | def revocation_basepoint(self) -> str:
34 | return self.raw_revocation_basepoint().format().hex()
35 |
36 | def raw_delayed_payment_basepoint(self) -> coincurve.PublicKey:
37 | return coincurve.PublicKey.from_secret(self.delayed_payment_base_secret.secret)
38 |
39 | def delayed_payment_basepoint(self) -> str:
40 | return self.raw_delayed_payment_basepoint().format().hex()
41 |
42 | def raw_htlc_basepoint(self) -> coincurve.PublicKey:
43 | return coincurve.PublicKey.from_secret(self.htlc_base_secret.secret)
44 |
45 | def htlc_basepoint(self) -> str:
46 | return self.raw_htlc_basepoint().format().hex()
47 |
48 | def raw_per_commit_secret(self, n: int) -> coincurve.PrivateKey:
49 | # BOLT #3:
50 | # The first secret used:
51 | # - MUST be index 281474976710655,
52 | # - and from there, the index is decremented.
53 | if n > 281474976710655:
54 | raise ValueError("48 bits is all you get!")
55 | index = 281474976710655 - n
56 |
57 | # BOLT #3:
58 | # generate_from_seed(seed, I):
59 | # P = seed
60 | # for B in 47 down to 0:
61 | # if B set in I:
62 | # flip(B) in P
63 | # P = SHA256(P)
64 | # return P
65 | # ```
66 |
67 | # FIXME: This is the updated wording from PR #779
68 | # Where "flip(B)" alternates the (B mod 8)'th bit of the (B div 8)'th
69 | # byte of the value. So, "flip(0) in e3b0..." is "e2b0...", and
70 | # "flip(10) in "e3b0..." is "e3b4".
71 | P = bytearray(self.shachain_seed)
72 | for B in range(47, -1, -1):
73 | if ((1 << B) & index) != 0:
74 | P[B // 8] ^= 1 << (B % 8)
75 | P = bytearray(hashlib.sha256(P).digest())
76 |
77 | return coincurve.PrivateKey(P)
78 |
79 | def per_commit_secret(self, n: int) -> str:
80 | return self.raw_per_commit_secret(n).secret.hex()
81 |
82 | def raw_per_commit_point(self, n: int) -> coincurve.PublicKey:
83 | return coincurve.PublicKey.from_secret(self.raw_per_commit_secret(n).secret)
84 |
85 | def per_commit_point(self, n: int) -> str:
86 | return self.raw_per_commit_point(n).format().hex()
87 |
88 |
89 | def test_shachain() -> None:
90 | # BOLT #3:
91 | # ## Generation Tests
92 | # name: generate_from_seed 0 final node
93 | # seed: 0x0000000000000000000000000000000000000000000000000000000000000000
94 | # I: 281474976710655
95 | # output: 0x02a40c85b6f28da08dfdbe0926c53fab2de6d28c10301f8f7c4073d5e42e3148
96 | keyset = KeySet(
97 | "01",
98 | "01",
99 | "01",
100 | "01",
101 | "0000000000000000000000000000000000000000000000000000000000000000",
102 | )
103 | assert (
104 | keyset.per_commit_secret(0xFFFFFFFFFFFF - 281474976710655)
105 | == "02a40c85b6f28da08dfdbe0926c53fab2de6d28c10301f8f7c4073d5e42e3148"
106 | )
107 |
108 | # BOLT #3:
109 | # name: generate_from_seed FF final node
110 | # seed: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
111 | # I: 281474976710655
112 | # output: 0x7cc854b54e3e0dcdb010d7a3fee464a9687be6e8db3be6854c475621e007a5dc
113 | keyset = KeySet(
114 | "01",
115 | "01",
116 | "01",
117 | "01",
118 | "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
119 | )
120 | assert (
121 | keyset.per_commit_secret(0xFFFFFFFFFFFF - 281474976710655)
122 | == "7cc854b54e3e0dcdb010d7a3fee464a9687be6e8db3be6854c475621e007a5dc"
123 | )
124 |
125 | # BOLT #3:
126 | # name: generate_from_seed FF alternate bits 1
127 | # seed: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
128 | # I: 0xaaaaaaaaaaa
129 | # output: 0x56f4008fb007ca9acf0e15b054d5c9fd12ee06cea347914ddbaed70d1c13a528
130 | keyset = KeySet(
131 | "01",
132 | "01",
133 | "01",
134 | "01",
135 | "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
136 | )
137 | assert (
138 | keyset.per_commit_secret(0xFFFFFFFFFFFF - 0xAAAAAAAAAAA)
139 | == "56f4008fb007ca9acf0e15b054d5c9fd12ee06cea347914ddbaed70d1c13a528"
140 | )
141 |
142 | # BOLT #3:
143 | # name: generate_from_seed FF alternate bits 2
144 | # seed: 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
145 | # I: 0x555555555555
146 | # output: 0x9015daaeb06dba4ccc05b91b2f73bd54405f2be9f217fbacd3c5ac2e62327d31
147 | keyset = KeySet(
148 | "01",
149 | "01",
150 | "01",
151 | "01",
152 | "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
153 | )
154 | assert (
155 | keyset.per_commit_secret(0xFFFFFFFFFFFF - 0x555555555555)
156 | == "9015daaeb06dba4ccc05b91b2f73bd54405f2be9f217fbacd3c5ac2e62327d31"
157 | )
158 |
159 | # BOLT #3:
160 | # name: generate_from_seed 01 last nontrivial node
161 | # seed: 0x0101010101010101010101010101010101010101010101010101010101010101
162 | # I: 1
163 | # output: 0x915c75942a26bb3a433a8ce2cb0427c29ec6c1775cfc78328b57f6ba7bfeaa9c
164 | keyset = KeySet(
165 | "01",
166 | "01",
167 | "01",
168 | "01",
169 | "0101010101010101010101010101010101010101010101010101010101010101",
170 | )
171 | assert (
172 | keyset.per_commit_secret(0xFFFFFFFFFFFF - 1)
173 | == "915c75942a26bb3a433a8ce2cb0427c29ec6c1775cfc78328b57f6ba7bfeaa9c"
174 | )
175 |
--------------------------------------------------------------------------------
/lnprototest/namespace.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/python3
2 | import pyln.spec.bolt1
3 | import pyln.spec.bolt2
4 | import pyln.spec.bolt7
5 | from pyln.proto.message import MessageNamespace
6 | from .signature import SigType
7 | from typing import List
8 |
9 |
10 | def make_namespace(csv: List[str]) -> MessageNamespace:
11 | """Load a namespace, replacing signature type"""
12 | ns = MessageNamespace()
13 | # We replace the fundamental signature type with our custom type,
14 | # then we load in all the csv files so they use it.
15 | ns.fundamentaltypes["signature"] = SigType()
16 | ns.load_csv(csv)
17 | return ns
18 |
19 |
20 | def peer_message_namespace() -> MessageNamespace:
21 | """Namespace containing all the peer messages"""
22 | return make_namespace(
23 | pyln.spec.bolt1.csv + pyln.spec.bolt2.csv + pyln.spec.bolt7.csv
24 | )
25 |
26 |
27 | def namespace() -> MessageNamespace:
28 | return event_namespace
29 |
30 |
31 | def assign_namespace(ns: MessageNamespace) -> None:
32 | global event_namespace
33 | event_namespace = ns
34 |
35 |
36 | # By default, include all peer message bolts
37 | event_namespace = peer_message_namespace()
38 |
--------------------------------------------------------------------------------
/lnprototest/proposals.py:
--------------------------------------------------------------------------------
1 | dual_fund_csv = [
2 | "msgtype,tx_add_input,66",
3 | "msgdata,tx_add_input,channel_id,channel_id,",
4 | "msgdata,tx_add_input,serial_id,u64,",
5 | "msgdata,tx_add_input,prevtx_len,u16,",
6 | "msgdata,tx_add_input,prevtx,byte,prevtx_len",
7 | "msgdata,tx_add_input,prevtx_vout,u32,",
8 | "msgdata,tx_add_input,sequence,u32,",
9 | "msgdata,tx_add_input,script_sig_len,u16,",
10 | "msgdata,tx_add_input,script_sig,byte,script_sig_len",
11 | "msgtype,tx_add_output,67",
12 | "msgdata,tx_add_output,channel_id,channel_id,",
13 | "msgdata,tx_add_output,serial_id,u64,",
14 | "msgdata,tx_add_output,sats,u64,",
15 | "msgdata,tx_add_output,scriptlen,u16,",
16 | "msgdata,tx_add_output,script,byte,scriptlen",
17 | "msgtype,tx_remove_input,68",
18 | "msgdata,tx_remove_input,channel_id,channel_id,",
19 | "msgdata,tx_remove_input,serial_id,u64,",
20 | "msgtype,tx_remove_output,69",
21 | "msgdata,tx_remove_output,channel_id,channel_id,",
22 | "msgdata,tx_remove_output,serial_id,u64,",
23 | "msgtype,tx_complete,70",
24 | "msgdata,tx_complete,channel_id,channel_id,",
25 | "msgtype,tx_signatures,71",
26 | "msgdata,tx_signatures,channel_id,channel_id,",
27 | "msgdata,tx_signatures,txid,sha256,",
28 | "msgdata,tx_signatures,num_witnesses,u16,",
29 | "msgdata,tx_signatures,witness_stack,witness_stack,num_witnesses",
30 | "subtype,witness_stack",
31 | "subtypedata,witness_stack,num_input_witness,u16,",
32 | "subtypedata,witness_stack,witness_element,witness_element,num_input_witness",
33 | "subtype,witness_element",
34 | "subtypedata,witness_element,len,u16,",
35 | "subtypedata,witness_element,witness,byte,len",
36 | "msgtype,open_channel2,64",
37 | "msgdata,open_channel2,chain_hash,chain_hash,",
38 | "msgdata,open_channel2,channel_id,channel_id,",
39 | "msgdata,open_channel2,funding_feerate_perkw,u32,",
40 | "msgdata,open_channel2,commitment_feerate_perkw,u32,",
41 | "msgdata,open_channel2,funding_satoshis,u64,",
42 | "msgdata,open_channel2,dust_limit_satoshis,u64,",
43 | "msgdata,open_channel2,max_htlc_value_in_flight_msat,u64,",
44 | "msgdata,open_channel2,htlc_minimum_msat,u64,",
45 | "msgdata,open_channel2,to_self_delay,u16,",
46 | "msgdata,open_channel2,max_accepted_htlcs,u16,",
47 | "msgdata,open_channel2,locktime,u32,",
48 | "msgdata,open_channel2,funding_pubkey,point,",
49 | "msgdata,open_channel2,revocation_basepoint,point,",
50 | "msgdata,open_channel2,payment_basepoint,point,",
51 | "msgdata,open_channel2,delayed_payment_basepoint,point,",
52 | "msgdata,open_channel2,htlc_basepoint,point,",
53 | "msgdata,open_channel2,first_per_commitment_point,point,",
54 | "msgdata,open_channel2,channel_flags,byte,",
55 | "msgdata,open_channel2,tlvs,opening_tlvs,",
56 | "tlvtype,opening_tlvs,option_upfront_shutdown_script,1",
57 | "tlvdata,opening_tlvs,option_upfront_shutdown_script,shutdown_len,u16,",
58 | "tlvdata,opening_tlvs,option_upfront_shutdown_script,shutdown_scriptpubkey,byte,shutdown_len",
59 | "msgtype,accept_channel2,65",
60 | "msgdata,accept_channel2,channel_id,channel_id,",
61 | "msgdata,accept_channel2,funding_satoshis,u64,",
62 | "msgdata,accept_channel2,dust_limit_satoshis,u64,",
63 | "msgdata,accept_channel2,max_htlc_value_in_flight_msat,u64,",
64 | "msgdata,accept_channel2,htlc_minimum_msat,u64,",
65 | "msgdata,accept_channel2,minimum_depth,u32,",
66 | "msgdata,accept_channel2,to_self_delay,u16,",
67 | "msgdata,accept_channel2,max_accepted_htlcs,u16,",
68 | "msgdata,accept_channel2,funding_pubkey,point,",
69 | "msgdata,accept_channel2,revocation_basepoint,point,",
70 | "msgdata,accept_channel2,payment_basepoint,point,",
71 | "msgdata,accept_channel2,delayed_payment_basepoint,point,",
72 | "msgdata,accept_channel2,htlc_basepoint,point,",
73 | "msgdata,accept_channel2,first_per_commitment_point,point,",
74 | "msgdata,accept_channel2,tlvs,accept_tlvs,",
75 | "tlvtype,accept_tlvs,option_upfront_shutdown_script,1",
76 | "tlvdata,accept_tlvs,option_upfront_shutdown_script,shutdown_len,u16,",
77 | "tlvdata,accept_tlvs,option_upfront_shutdown_script,shutdown_scriptpubkey,byte,shutdown_len",
78 | "msgtype,init_rbf,72",
79 | "msgdata,init_rbf,channel_id,channel_id,",
80 | "msgdata,init_rbf,funding_satoshis,u64,",
81 | "msgdata,init_rbf,locktime,u32,",
82 | "msgdata,init_rbf,funding_feerate_perkw,u32,",
83 | "msgtype,ack_rbf,73",
84 | "msgdata,ack_rbf,channel_id,channel_id,",
85 | "msgdata,ack_rbf,funding_satoshis,u64,",
86 | ]
87 |
88 | # This is https://github.com/lightningnetwork/lightning-rfc/pull/880
89 | channel_type_csv = [
90 | "tlvtype,open_channel_tlvs,channel_type,1",
91 | "tlvdata,open_channel_tlvs,channel_type,type,byte,...",
92 | "tlvtype,accept_channel_tlvs,channel_type,1",
93 | "tlvdata,accept_channel_tlvs,channel_type,type,byte,...",
94 | ]
95 |
--------------------------------------------------------------------------------
/lnprototest/runner.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/python3
2 | import logging
3 | import shutil
4 | import tempfile
5 |
6 | import coincurve
7 | import functools
8 |
9 | from .bitfield import bitfield
10 | from .errors import SpecFileError
11 | from .structure import Sequence
12 | from .event import Event, MustNotMsg, ExpectMsg
13 | from .utils import privkey_expand
14 | from .keyset import KeySet
15 | from abc import ABC, abstractmethod
16 | from typing import Dict, Optional, List, Union, Any, Callable
17 |
18 |
19 | class Conn(object):
20 | """Class for connections. Details filled in by the particular runner."""
21 |
22 | def __init__(self, connprivkey: str):
23 | """Create a connection from a node with the given hex privkey: we use
24 | trivial values for private keys, so we simply left-pad with zeroes"""
25 | self.name = connprivkey
26 | self.connprivkey = privkey_expand(connprivkey)
27 | self.pubkey = coincurve.PublicKey.from_secret(self.connprivkey.secret)
28 | self.expected_error = False
29 | self.must_not_events: List[MustNotMsg] = []
30 |
31 | def __str__(self) -> str:
32 | return self.name
33 |
34 |
35 | class Runner(ABC):
36 | """Abstract base class for runners.
37 |
38 | Most of the runner parameters can be extracted at runtime, but we do
39 | require that minimum_depth be 3, just for test simplicity.
40 | """
41 |
42 | def __init__(self, config: Any):
43 | self.config = config
44 | self.directory = tempfile.mkdtemp(prefix="lnpt-cl-")
45 | # key == connprivkey, value == Conn
46 | self.conns: Dict[str, Conn] = {}
47 | self.last_conn: Optional[Conn] = None
48 | self.stash: Dict[str, Dict[str, Any]] = {}
49 | self.logger = logging.getLogger(__name__)
50 | if self.config.getoption("verbose"):
51 | self.logger.setLevel(logging.DEBUG)
52 | else:
53 | self.logger.setLevel(logging.INFO)
54 |
55 | def _is_dummy(self) -> bool:
56 | """The DummyRunner returns True here, as it can't do some things"""
57 | return False
58 |
59 | def find_conn(self, connprivkey: Optional[str]) -> Optional[Conn]:
60 | # Default is whatever we specified last.
61 | if connprivkey is None:
62 | return self.last_conn
63 | if connprivkey in self.conns:
64 | self.last_conn = self.conns[connprivkey]
65 | return self.last_conn
66 | return None
67 |
68 | def add_conn(self, conn: Conn) -> None:
69 | self.conns[conn.name] = conn
70 | self.last_conn = conn
71 |
72 | def disconnect(self, event: Event, conn: Conn) -> None:
73 | if conn is None:
74 | raise SpecFileError(event, "Unknown conn")
75 | del self.conns[conn.name]
76 | self.check_final_error(event, conn, conn.expected_error, conn.must_not_events)
77 |
78 | def check_error(self, event: Event, conn: Conn) -> Optional[str]:
79 | conn.expected_error = True
80 | return None
81 |
82 | def post_check(self, sequence: Sequence) -> None:
83 | # Make sure no connection had an error.
84 | for conn_name in list(self.conns.keys()):
85 | logging.debug(
86 | f"disconnection connection with key={conn_name} and value={self.conns[conn_name]}"
87 | )
88 | self.disconnect(sequence, self.conns[conn_name])
89 |
90 | def restart(self) -> None:
91 | self.conns = {}
92 | self.last_conn = None
93 | self.stash = {}
94 |
95 | # FIXME: Why can't we use SequenceUnion here?
96 | def run(self, events: Union[Sequence, List[Event], Event]) -> None:
97 | sequence = Sequence(events)
98 | self.start()
99 | while True:
100 | all_done = sequence.action(self)
101 | self.post_check(sequence)
102 | if all_done:
103 | self.stop()
104 | return
105 | self.restart()
106 |
107 | def add_stash(self, stashname: str, vals: Any) -> None:
108 | """Add a dict to the stash."""
109 | self.stash[stashname] = vals
110 |
111 | def get_stash(self, event: Event, stashname: str, default: Any = None) -> Any:
112 | """Get an entry from the stash."""
113 | if stashname not in self.stash:
114 | if default is not None:
115 | return default
116 | raise SpecFileError(event, "Unknown stash name {}".format(stashname))
117 | return self.stash[stashname]
118 |
119 | def teardown(self):
120 | """The Teardown method is called at the end of the test,
121 | and it is used to clean up the root dir where the tests are run."""
122 | shutil.rmtree(self.directory)
123 |
124 | def runner_features(
125 | self,
126 | additional_features: Optional[List[int]] = None,
127 | globals: bool = False,
128 | ) -> str:
129 | """
130 | Provide the features required by the node.
131 | """
132 | if additional_features is None:
133 | return ""
134 | else:
135 | return bitfield(*additional_features)
136 |
137 | @abstractmethod
138 | def is_running(self) -> bool:
139 | """Return a boolean value that tells whether the runner is running
140 | or not.
141 | Is leave up to the runner implementation to keep the runner state"""
142 | pass
143 |
144 | @abstractmethod
145 | def connect(self, event: Event, connprivkey: str) -> None:
146 | pass
147 |
148 | @abstractmethod
149 | def check_final_error(
150 | self,
151 | event: Event,
152 | conn: Conn,
153 | expected: bool,
154 | must_not_events: List[MustNotMsg],
155 | ) -> None:
156 | pass
157 |
158 | @abstractmethod
159 | def start(self) -> None:
160 | pass
161 |
162 | @abstractmethod
163 | def stop(self, print_logs: bool = False) -> None:
164 | """
165 | Stop the runner, and print all the log that the ln
166 | implementation produced.
167 |
168 | Print the log is useful when we have a failure e we need
169 | to debug what happens during the tests.
170 | """
171 | pass
172 |
173 | @abstractmethod
174 | def recv(self, event: Event, conn: Conn, outbuf: bytes) -> None:
175 | pass
176 |
177 | @abstractmethod
178 | def get_output_message(self, conn: Conn, event: ExpectMsg) -> Optional[bytes]:
179 | pass
180 |
181 | @abstractmethod
182 | def getblockheight(self) -> int:
183 | pass
184 |
185 | @abstractmethod
186 | def trim_blocks(self, newheight: int) -> None:
187 | pass
188 |
189 | @abstractmethod
190 | def add_blocks(self, event: Event, txs: List[str], n: int) -> None:
191 | pass
192 |
193 | @abstractmethod
194 | def expect_tx(self, event: Event, txid: str) -> None:
195 | pass
196 |
197 | @abstractmethod
198 | def invoice(self, event: Event, amount: int, preimage: str) -> None:
199 | pass
200 |
201 | @abstractmethod
202 | def accept_add_fund(self, event: Event) -> None:
203 | pass
204 |
205 | @abstractmethod
206 | def fundchannel(
207 | self,
208 | event: Event,
209 | conn: Conn,
210 | amount: int,
211 | feerate: int = 0,
212 | expect_fail: bool = False,
213 | ) -> None:
214 | pass
215 |
216 | @abstractmethod
217 | def init_rbf(
218 | self,
219 | event: Event,
220 | conn: Conn,
221 | channel_id: str,
222 | amount: int,
223 | utxo_txid: str,
224 | utxo_outnum: int,
225 | feerate: int,
226 | ) -> None:
227 | pass
228 |
229 | @abstractmethod
230 | def addhtlc(self, event: Event, conn: Conn, amount: int, preimage: str) -> None:
231 | pass
232 |
233 | @abstractmethod
234 | def get_keyset(self) -> KeySet:
235 | pass
236 |
237 | @abstractmethod
238 | def get_node_privkey(self) -> str:
239 | pass
240 |
241 | @abstractmethod
242 | def get_node_bitcoinkey(self) -> str:
243 | pass
244 |
245 | @abstractmethod
246 | def has_option(self, optname: str) -> Optional[str]:
247 | pass
248 |
249 | @abstractmethod
250 | def add_startup_flag(self, flag: str) -> None:
251 | pass
252 |
253 | @abstractmethod
254 | def close_channel(self, channel_id: str) -> None:
255 | """
256 | Close the channel with the specified channel id.
257 |
258 | :param channel_id: the channel id as string value where the
259 | caller want to close;
260 | :return No value in case of success is expected,
261 | but an `RpcError` is expected in case of err.
262 | """
263 | pass
264 |
265 |
266 | def remote_revocation_basepoint() -> Callable[[Runner, Event, str], str]:
267 | """Get the remote revocation basepoint"""
268 |
269 | def _remote_revocation_basepoint(runner: Runner, event: Event, field: str) -> str:
270 | return runner.get_keyset().revocation_basepoint()
271 |
272 | return _remote_revocation_basepoint
273 |
274 |
275 | def remote_payment_basepoint() -> Callable[[Runner, Event, str], str]:
276 | """Get the remote payment basepoint"""
277 |
278 | def _remote_payment_basepoint(runner: Runner, event: Event, field: str) -> str:
279 | return runner.get_keyset().payment_basepoint()
280 |
281 | return _remote_payment_basepoint
282 |
283 |
284 | def remote_delayed_payment_basepoint() -> Callable[[Runner, Event, str], str]:
285 | """Get the remote delayed_payment basepoint"""
286 |
287 | def _remote_delayed_payment_basepoint(
288 | runner: Runner, event: Event, field: str
289 | ) -> str:
290 | return runner.get_keyset().delayed_payment_basepoint()
291 |
292 | return _remote_delayed_payment_basepoint
293 |
294 |
295 | def remote_htlc_basepoint() -> Callable[[Runner, Event, str], str]:
296 | """Get the remote htlc basepoint"""
297 |
298 | def _remote_htlc_basepoint(runner: Runner, event: Event, field: str) -> str:
299 | return runner.get_keyset().htlc_basepoint()
300 |
301 | return _remote_htlc_basepoint
302 |
303 |
304 | def remote_funding_pubkey() -> Callable[[Runner, Event, str], str]:
305 | """Get the remote funding pubkey (FIXME: we assume there's only one!)"""
306 |
307 | def _remote_funding_pubkey(runner: Runner, event: Event, field: str) -> str:
308 | return (
309 | coincurve.PublicKey.from_secret(
310 | privkey_expand(runner.get_node_bitcoinkey()).secret
311 | )
312 | .format()
313 | .hex()
314 | )
315 |
316 | return _remote_funding_pubkey
317 |
318 |
319 | def remote_funding_privkey() -> Callable[[Runner, Event, str], str]:
320 | """Get the remote funding privkey (FIXME: we assume there's only one!)"""
321 |
322 | def _remote_funding_privkey(runner: Runner, event: Event, field: str) -> str:
323 | return runner.get_node_bitcoinkey()
324 |
325 | return _remote_funding_privkey
326 |
327 |
328 | def remote_per_commitment_point(n: int) -> Callable[[Runner, Event, str], str]:
329 | """Get the n'th remote per-commitment point"""
330 |
331 | def _remote_per_commitment_point(
332 | n: int, runner: Runner, event: Event, field: str
333 | ) -> str:
334 | return runner.get_keyset().per_commit_point(n)
335 |
336 | return functools.partial(_remote_per_commitment_point, n)
337 |
338 |
339 | def remote_per_commitment_secret(n: int) -> Callable[[Runner, Event, str], str]:
340 | """Get the n'th remote per-commitment secret"""
341 |
342 | def _remote_per_commitment_secret(runner: Runner, event: Event, field: str) -> str:
343 | return runner.get_keyset().per_commit_secret(n)
344 |
345 | return _remote_per_commitment_secret
346 |
--------------------------------------------------------------------------------
/lnprototest/signature.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/python3
2 | import coincurve
3 | from io import BufferedIOBase
4 | from pyln.proto.message import FieldType, split_field
5 | from .utils import check_hex, privkey_expand
6 | from typing import Union, Tuple, Dict, Any, Optional, cast
7 |
8 |
9 | class Sig(object):
10 | """The value of a signature, either as a privkey/hash pair or a raw
11 | signature. This has the property that if the raw signature is a valid
12 | signature of privkey over hash, they are considered "equal"
13 | """
14 |
15 | def __init__(self, *args: Any):
16 | """Either a 64-byte hex/bytes value, or a PrivateKey and a hash"""
17 | if len(args) == 1:
18 | if type(args[0]) is bytes:
19 | if len(args[0]) != 64:
20 | raise ValueError("Sig() with 1 arg expects 64 bytes or 128 hexstr")
21 | self.sigval: Union[bytes, None] = cast(bytes, args[0])
22 | else:
23 | if type(args[0]) is not str:
24 | raise TypeError("Expected hexsig or Privkey, hash")
25 | if len(args[0]) != 128:
26 | self.sigval = Sig.from_der(bytes.fromhex(args[0]))
27 | else:
28 | self.sigval = bytes.fromhex(args[0])
29 | elif len(args) == 2:
30 | self.sigval = None
31 | self.privkey = privkey_expand(args[0])
32 | self.hashval = bytes.fromhex(check_hex(args[1], 64))
33 | else:
34 | raise TypeError("Expected hexsig or Privkey, hash")
35 |
36 | @staticmethod
37 | def to_der(b: bytes) -> bytes:
38 | """Seriously fuck off with DER encoding :("""
39 | r = b[0:32]
40 | s = b[32:64]
41 | # Trim zero bytes
42 | while r[0] == 0:
43 | r = r[1:]
44 | # Prepend 0 again if would be negative
45 | if r[0] & 0x80:
46 | r = bytes([0]) + r
47 | # Trim zero bytes
48 | while s[0] == 0:
49 | s = s[1:]
50 | # Prepend 0 again if would be negative
51 | if s[0] & 0x80:
52 | s = bytes([0]) + s
53 |
54 | # 2 == integer, next == length
55 | ret = bytes([0x02, len(r)]) + r + bytes([0x02, len(s)]) + s
56 | # 30 == compound, next = length
57 | return bytes([0x30, len(ret)]) + ret
58 |
59 | @staticmethod
60 | def from_der(b: bytes) -> bytes:
61 | """Sigh. Seriously, WTF is it with DER encoding?"""
62 | if b[0] != 0x30 or b[1] != len(b) - 2 or b[2] != 0x02:
63 | raise ValueError("{} is not a DER signature?".format(b.hex()))
64 | rlen = b[3]
65 | r = b[4 : 4 + rlen].rjust(32, bytes(1))[-32:]
66 | assert len(r) == 32
67 | if b[4 + rlen] != 0x02:
68 | raise ValueError("{} is not a DER signature?".format(b.hex()))
69 | s = b[4 + rlen + 1 + 1 :].rjust(32, bytes(1))[-32:]
70 | assert len(s) == 32
71 | return r + s
72 |
73 | def __eq__(self, other: Any) -> bool:
74 | # For convenience of using stashed objects, we allow comparison with str
75 | if isinstance(other, str):
76 | othersig = Sig(other)
77 | else:
78 | othersig = cast(Sig, other)
79 | if self.sigval and othersig.sigval:
80 | return self.sigval == othersig.sigval
81 | elif not self.sigval and not othersig.sigval:
82 | return self.privkey == othersig.privkey and self.hashval == othersig.hashval
83 | elif not self.sigval:
84 | a = self
85 | b = othersig
86 | else:
87 | a = othersig
88 | b = self
89 | # A has a privkey/hash, B has a sigval.
90 | pubkey = coincurve.PublicKey.from_secret(a.privkey.secret)
91 | assert b.sigval is not None
92 | if coincurve.verify_signature(
93 | self.to_der(b.sigval), a.hashval, pubkey.format(), hasher=None
94 | ):
95 | return True
96 | return False
97 |
98 | def to_str(self) -> str:
99 | if self.sigval:
100 | return self.sigval.hex()
101 | else:
102 | return "Sig({},{})".format(self.privkey.secret.hex(), self.hashval.hex())
103 |
104 | @staticmethod
105 | def from_str(s: str) -> Tuple["Sig", str]:
106 | a, b = split_field(s)
107 | if a.startswith("Sig("):
108 | privkey = a[4:]
109 | a, b = split_field(b[1:])
110 | # Trim ) off Sig()
111 | return Sig(privkey, a[:-1]), b
112 | return Sig(bytes.fromhex(a)), b
113 |
114 | def to_bin(self) -> bytes:
115 | if not self.sigval:
116 | return self.from_der(self.privkey.sign(self.hashval, hasher=None))
117 | else:
118 | return self.sigval
119 |
120 |
121 | class SigType(FieldType):
122 | """A signature type which has special comparison properties"""
123 |
124 | def __init__(self) -> None:
125 | super().__init__("signature")
126 |
127 | def val_to_str(self, v: Sig, otherfields: Dict[str, Any]) -> str:
128 | return v.to_str()
129 |
130 | def val_from_str(self, s: str) -> Tuple["Sig", str]:
131 | return Sig.from_str(s)
132 |
133 | def write(
134 | self, io_out: BufferedIOBase, v: Sig, otherfields: Dict[str, Any]
135 | ) -> None:
136 | io_out.write(v.to_bin())
137 |
138 | def read(self, io_in: BufferedIOBase, otherfields: Dict[str, Any]) -> Optional[Sig]:
139 | val = io_in.read(64)
140 | if len(val) == 0:
141 | return None
142 | elif len(val) != 64:
143 | raise ValueError("{}: not enough remaining".format(self))
144 | return Sig(val)
145 |
146 |
147 | def test_der() -> None:
148 | der = b"0E\x02!\x00\xa0\xb3\x7f\x8f\xbah<\xc6\x8fet\xcdC\xb3\x9f\x03C\xa5\x00\x08\xbfl\xce\xa9\xd121\xd9\xe7\xe2\xe1\xe4\x02 \x11\xed\xc8\xd3\x07%B\x96&J\xeb\xfc=\xc7l\xd8\xb6h7:\x07/\xd6Fe\xb5\x00\x00\xe9\xfc\xceR"
149 | sig = Sig.from_der(der)
150 | der2 = Sig.to_der(sig)
151 | assert der == der2
152 |
153 |
154 | def test_signature() -> None:
155 | s = Sig("01", "00" * 32)
156 |
157 | assert s == s
158 | b = s.to_bin()
159 | s2 = Sig(b)
160 |
161 | assert s == s2
162 | assert s2 == s
163 |
--------------------------------------------------------------------------------
/lnprototest/stash/__init__.py:
--------------------------------------------------------------------------------
1 | """stash provides functions which resolve at action time.
2 |
3 | During a run, Events often put state in the runner object for later
4 | events to access at action time. This is called the "stash".
5 |
6 | These accessors actually return functions, which most Events know
7 | means they're to be called at action time. For the sake of (perhaps
8 | non-Pythony) test authors, they are always called as (), even if they
9 | simply return another function.
10 |
11 | """
12 |
13 | from .stash import (
14 | commitsig_to_send,
15 | commitsig_to_recv,
16 | channel_id,
17 | channel_announcement,
18 | channel_update,
19 | get_member,
20 | rcvd,
21 | sent,
22 | funding_amount,
23 | funding_pubkey,
24 | funding_tx,
25 | funding_txid,
26 | funding,
27 | funding_close_tx,
28 | htlc_sigs_to_send,
29 | htlc_sigs_to_recv,
30 | locking_script,
31 | witnesses,
32 | stash_field_from_event,
33 | )
34 |
--------------------------------------------------------------------------------
/lnprototest/stash/stash.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import time
3 | import coincurve
4 |
5 | from typing import Callable, Optional, Any
6 | from pyln.proto.message import Message
7 |
8 | from lnprototest import Runner, Event, Side, SpecFileError, Funding
9 |
10 |
11 | def commitsig_to_send() -> Callable[[Runner, Event, str], str]:
12 | """Get the appropriate signature for the local side to send to the remote"""
13 |
14 | def _commitsig_to_send(runner: Runner, event: Event, field: str) -> str:
15 | tx = runner.get_stash(event, "Commit").remote_unsigned_tx()
16 | return runner.get_stash(event, "Commit").local_sig(tx)
17 |
18 | return _commitsig_to_send
19 |
20 |
21 | def commitsig_to_recv() -> Callable[[Runner, Event, str], str]:
22 | """Get the appropriate signature for the remote side to send to the local"""
23 |
24 | def _commitsig_to_recv(runner: Runner, event: Event, field: str) -> str:
25 | tx = runner.get_stash(event, "Commit").local_unsigned_tx()
26 | return runner.get_stash(event, "Commit").remote_sig(tx)
27 |
28 | return _commitsig_to_recv
29 |
30 |
31 | def _htlc_sigs(signer: Side, runner: Runner, event: Event, field: str) -> str:
32 | sigs = runner.get_stash(event, "Commit").htlc_sigs(signer, not signer)
33 | return "[" + ",".join([sig.to_str() for sig in sigs]) + "]"
34 |
35 |
36 | def htlc_sigs_to_send() -> Callable[[Runner, Event, str], str]:
37 | """Get the HTLC signatures for local side to send to the remote"""
38 | return functools.partial(_htlc_sigs, Side.local)
39 |
40 |
41 | def htlc_sigs_to_recv() -> Callable[[Runner, Event, str], str]:
42 | """Get the HTLC signatures for remote side to send to the local"""
43 | return functools.partial(_htlc_sigs, Side.remote)
44 |
45 |
46 | def channel_id() -> Callable[[Runner, Event, str], str]:
47 | """Get the channel_id for the current Commit"""
48 |
49 | def _channel_id(runner: Runner, event: Event, field: str) -> str:
50 | return runner.get_stash(event, "Commit").funding.channel_id()
51 |
52 | return _channel_id
53 |
54 |
55 | def channel_id_v2() -> Callable[[Runner, Event, str], str]:
56 | """Get the channel_id for the current Commit for a v2 channel open"""
57 |
58 | def _channel_id(runner: Runner, event: Event, field: str) -> str:
59 | return runner.get_stash(event, "Commit").channel_id_v2()
60 |
61 | return _channel_id
62 |
63 |
64 | def channel_announcement(
65 | short_channel_id: str, features: bytes
66 | ) -> Callable[[Runner, Event, str], str]:
67 | """Get the channel_announcement for the current Commit"""
68 |
69 | def _channel_announcement(
70 | short_channel_id: str, features: bytes, runner: Runner, event: Event, field: str
71 | ) -> Message:
72 | return runner.get_stash(event, "Commit").channel_announcement(
73 | short_channel_id, features
74 | )
75 |
76 | return functools.partial(_channel_announcement, short_channel_id, features)
77 |
78 |
79 | def channel_update(
80 | short_channel_id: str,
81 | side: Side,
82 | disable: bool,
83 | cltv_expiry_delta: int,
84 | htlc_minimum_msat: int,
85 | fee_base_msat: int,
86 | fee_proportional_millionths: int,
87 | htlc_maximum_msat: Optional[int],
88 | timestamp: Optional[int] = None,
89 | ) -> Callable[[Runner, Event, str], str]:
90 | """Get a channel_update for the current Commit"""
91 |
92 | def _channel_update(
93 | short_channel_id: str,
94 | side: Side,
95 | disable: bool,
96 | cltv_expiry_delta: int,
97 | htlc_minimum_msat: int,
98 | fee_base_msat: int,
99 | fee_proportional_millionths: int,
100 | timestamp: Optional[int],
101 | htlc_maximum_msat: Optional[int],
102 | runner: Runner,
103 | event: Event,
104 | field: str,
105 | ) -> Message:
106 | """Get the channel_update"""
107 | if timestamp is None:
108 | timestamp = int(time.time())
109 | return runner.get_stash(event, "Commit").channel_update(
110 | short_channel_id,
111 | side,
112 | disable,
113 | cltv_expiry_delta,
114 | htlc_maximum_msat,
115 | fee_base_msat,
116 | fee_proportional_millionths,
117 | timestamp,
118 | htlc_maximum_msat,
119 | )
120 |
121 | return functools.partial(
122 | _channel_update,
123 | short_channel_id,
124 | side,
125 | disable,
126 | cltv_expiry_delta,
127 | htlc_minimum_msat,
128 | fee_base_msat,
129 | fee_proportional_millionths,
130 | htlc_maximum_msat,
131 | timestamp,
132 | )
133 |
134 |
135 | def get_member(
136 | event: Event, runner: "Runner", stashname: str, var: str, last: bool = True
137 | ) -> str:
138 | """Get member field from stash for ExpectMsg or Msg.
139 |
140 | If var contains a '.' then we look for that message to extract the field. If last is True, we get the last message, otherwise the first.
141 | """
142 | stash = runner.get_stash(event, stashname)
143 | if "." in var:
144 | prevname, _, var = var.partition(".")
145 | else:
146 | prevname = ""
147 | if last:
148 | seq = reversed(stash)
149 | else:
150 | seq = stash
151 |
152 | for name, d in seq:
153 | if prevname == "" or name == prevname:
154 | if var not in d:
155 | raise SpecFileError(
156 | event,
157 | "{}: {} did not receive a {}".format(stashname, prevname, var),
158 | )
159 | return d[var]
160 | raise SpecFileError(event, "{}: have no prior {}".format(stashname, prevname))
161 |
162 |
163 | def _get_member(
164 | stashname: str,
165 | fieldname: Optional[str],
166 | casttype: Any,
167 | # This is the signature which Msg() expects for callable values:
168 | runner: "Runner",
169 | event: Event,
170 | field: str,
171 | ) -> Any:
172 | # If they don't specify fieldname, it's same as this field.
173 | if fieldname is None:
174 | fieldname = field
175 | strval = get_member(event, runner, stashname, fieldname)
176 | try:
177 | return casttype(strval)
178 | except ValueError:
179 | raise SpecFileError(
180 | event,
181 | "{}.{} is {}, not a valid {}".format(
182 | stashname, fieldname, strval, casttype
183 | ),
184 | )
185 |
186 |
187 | def rcvd(
188 | fieldname: Optional[str] = None, casttype: Any = str
189 | ) -> Callable[[Runner, Event, Any], Any]:
190 | """Use previous ExpectMsg field (as string)
191 |
192 | fieldname can be [msg].[field] or just [field] for last ExpectMsg
193 |
194 | """
195 | return functools.partial(_get_member, "ExpectMsg", fieldname, casttype)
196 |
197 |
198 | def sent(
199 | fieldname: Optional[str] = None, casttype: Any = str
200 | ) -> Callable[[Runner, Event, Any], Any]:
201 | """Use previous Msg field (as string)
202 |
203 | fieldname can be [msg].[field] or just [field] for last Msg
204 |
205 | """
206 | return functools.partial(_get_member, "Msg", fieldname, casttype)
207 |
208 |
209 | def funding_amount() -> Callable[[Runner, Event, str], int]:
210 | """Get the stashed funding amount"""
211 |
212 | def _funding_amount(runner: Runner, event: Event, field: str) -> int:
213 | return runner.get_stash(event, "Funding").amount
214 |
215 | return _funding_amount
216 |
217 |
218 | def funding_pubkey(side: Side) -> Callable[[Runner, Event, str], str]:
219 | """Get the stashed funding pubkey for side"""
220 |
221 | def _funding_pubkey(side: Side, runner: Runner, event: Event, field: str) -> str:
222 | return coincurve.PublicKey.from_secret(
223 | runner.get_stash(event, "Funding").funding_privkeys[side].secret
224 | )
225 |
226 | return functools.partial(_funding_pubkey, side)
227 |
228 |
229 | def funding_tx() -> Callable[[Runner, Event, str], str]:
230 | """Get the funding transaction (as stashed by CreateFunding)"""
231 |
232 | def _funding_tx(runner: Runner, event: Event, field: str) -> str:
233 | return runner.get_stash(event, "FundingTx")
234 |
235 | return _funding_tx
236 |
237 |
238 | def funding_txid() -> Callable[[Runner, Event, str], str]:
239 | """Get the stashed funding transaction id"""
240 |
241 | def _funding_txid(runner: Runner, event: Event, field: str) -> str:
242 | return runner.get_stash(event, "Funding").txid
243 |
244 | return _funding_txid
245 |
246 |
247 | def funding() -> Callable[[Runner, Event, str], Funding]:
248 | """Get the stashed Funding (as stashed by CreateFunding or AcceptFunding)"""
249 |
250 | def _funding(runner: Runner, event: Event, field: str) -> Funding:
251 | return runner.get_stash(event, "Funding")
252 |
253 | return _funding
254 |
255 |
256 | def witnesses() -> Callable[[Runner, Event, str], str]:
257 | """Get the witnesses for the stashed funding tx"""
258 |
259 | def _witnesses(runner: Runner, event: Event, field: str) -> str:
260 | funding = runner.get_stash(event, "Funding")
261 | return funding.our_witnesses()
262 |
263 | return _witnesses
264 |
265 |
266 | def locking_script() -> Callable[[Runner, Event, str], str]:
267 | def _locking_script(runner: Runner, event: Event, field: str) -> str:
268 | return runner.get_stash(event, "Funding").locking_script().hex()
269 |
270 | return _locking_script
271 |
272 |
273 | def funding_close_tx() -> Callable[[Runner, Event, str], str]:
274 | def _funding_close_tx(runner: Runner, event: Event, field: str) -> str:
275 | return runner.get_stash(event, "Funding").close_tx()
276 |
277 | return _funding_close_tx
278 |
279 |
280 | def stash_field_from_event(
281 | stash_key: str, field_name: Optional[str] = None, dummy_val: Optional[Any] = None
282 | ) -> Callable[[Runner, Event, str], str]:
283 | """Generic stash function to get the information back from a previous event"""
284 |
285 | def _stash_field_from_event(runner: Runner, event: Event, field: str) -> str:
286 | if runner._is_dummy():
287 | return dummy_val
288 | field = field if field_name is None else field_name
289 | return runner.get_stash(event, stash_key).fields[field]
290 |
291 | return _stash_field_from_event
292 |
--------------------------------------------------------------------------------
/lnprototest/structure.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/python3
2 | import io
3 | import logging
4 |
5 | from .event import Event, ExpectMsg, ResolvableBool
6 | from .errors import SpecFileError, EventError
7 | from .namespace import namespace
8 | from pyln.proto.message import Message
9 | from typing import Union, List, Optional, TYPE_CHECKING, cast
10 |
11 | if TYPE_CHECKING:
12 | # Otherwise a circular dependency
13 | from .runner import Runner, Conn
14 |
15 | # These can all be fed to a Sequence() initializer.
16 | SequenceUnion = Union["Sequence", List[Event], Event]
17 |
18 |
19 | class Sequence(Event):
20 | """A sequence of ordered events"""
21 |
22 | def __init__(
23 | self,
24 | events: Union["Sequence", List[Event], Event],
25 | enable: ResolvableBool = True,
26 | ):
27 | """Events can be a Sequence, a single Event, or a list of Events. If
28 | enable is False, this turns into a noop (e.g. if runner doesn't support
29 | it)."""
30 | super().__init__()
31 | self.enable = enable
32 | if type(events) is Sequence:
33 | # mypy gets upset because Sequence isn't defined yet.
34 | self.events = events.events # type: ignore
35 | self.enable = events.enable # type: ignore
36 | self.name = events.name # type: ignore
37 | elif isinstance(events, Event):
38 | self.events = [events]
39 | else:
40 | self.events = events
41 |
42 | def enabled(self, runner: "Runner") -> bool:
43 | return self.resolve_arg("enable", runner, self.enable)
44 |
45 | def action(self, runner: "Runner", skip_first: bool = False) -> bool:
46 | super().action(runner)
47 | all_done = True
48 | for e in self.events:
49 | logging.debug(f"receiving event {e}")
50 | if not e.enabled(runner):
51 | continue
52 | if skip_first:
53 | skip_first = False
54 | else:
55 | all_done &= e.action(runner)
56 | return all_done
57 |
58 | @staticmethod
59 | def ignored_by_all(
60 | msg: Message, sequences: List["Sequence"]
61 | ) -> Optional[List[Message]]:
62 | # If they all say the same thing, that's the answer.
63 | rets = [cast(ExpectMsg, s.events[0]).ignore(msg) for s in sequences]
64 | if all([ignored == rets[0] for ignored in rets[1:]]):
65 | return rets[0]
66 | return None
67 |
68 | @staticmethod
69 | def match_which_sequence(
70 | runner: "Runner", msg: Message, sequences: List["Sequence"]
71 | ) -> Optional["Sequence"]:
72 | """Return which sequence expects this msg, or None"""
73 |
74 | for s in sequences:
75 | failreason = cast(ExpectMsg, s.events[0]).message_match(runner, msg)
76 | if failreason is None:
77 | return s
78 |
79 | return None
80 |
81 |
82 | class OneOf(Event):
83 | """Event representing multiple possible sequences, one of which should happen"""
84 |
85 | def __init__(self, *args: SequenceUnion):
86 | super().__init__()
87 | self.sequences = []
88 | for s in args:
89 | seq = Sequence(s)
90 | if len(seq.events) == 0:
91 | raise ValueError("{} is an empty sequence".format(s))
92 | self.sequences.append(seq)
93 |
94 | def enabled_sequences(self, runner: "Runner") -> List[Sequence]:
95 | """Returns all enabled sequences"""
96 | return [s for s in self.sequences if s.enabled(runner)]
97 |
98 | def action(self, runner: "Runner") -> bool:
99 | super().action(runner)
100 |
101 | # Check they all use the same conn!
102 | conn: Optional[Conn] = None
103 | for s in self.sequences:
104 | c = cast(ExpectMsg, s.events[0]).find_conn(runner)
105 | if conn is None:
106 | conn = c
107 | elif c != conn:
108 | raise SpecFileError(self, "sequences do not all use the same conn?")
109 | assert conn
110 |
111 | while True:
112 | event = self.sequences[0].events[0]
113 | binmsg = runner.get_output_message(conn, event)
114 | if binmsg is None:
115 | raise EventError(self, f"Did not receive a message {event} from runner")
116 |
117 | try:
118 | msg = Message.read(namespace(), io.BytesIO(binmsg))
119 | except ValueError as ve:
120 | raise EventError(self, "Invalid msg {}: {}".format(binmsg.hex(), ve))
121 |
122 | ignored = Sequence.ignored_by_all(msg, self.enabled_sequences(runner))
123 | # If they gave us responses, send those now.
124 | if ignored is not None:
125 | for msg in ignored:
126 | binm = io.BytesIO()
127 | msg.write(binm)
128 | runner.recv(self, conn, binm.getvalue())
129 | continue
130 |
131 | seq = Sequence.match_which_sequence(
132 | runner, msg, self.enabled_sequences(runner)
133 | )
134 | if seq is not None:
135 | # We found the sequence, run it
136 | return seq.action(runner, skip_first=True)
137 |
138 | raise EventError(
139 | self,
140 | "None of the sequences {} matched {}".format(
141 | self.enabled_sequences(runner), msg.to_str()
142 | ),
143 | )
144 |
145 |
146 | class AnyOrder(Event):
147 | """Event representing multiple sequences, all of which should happen, but not defined which order they would happen"""
148 |
149 | def __init__(self, *args: SequenceUnion):
150 | super().__init__()
151 | self.sequences = []
152 | for s in args:
153 | seq = Sequence(s)
154 | if len(seq.events) == 0:
155 | raise ValueError("{} is an empty sequence".format(s))
156 | self.sequences.append(seq)
157 |
158 | def enabled_sequences(self, runner: "Runner") -> List[Sequence]:
159 | """Returns all enabled sequences"""
160 | return [s for s in self.sequences if s.enabled(runner)]
161 |
162 | def action(self, runner: "Runner") -> bool:
163 | super().action(runner)
164 |
165 | # Check they all use the same conn!
166 | conn = None
167 | for s in self.sequences:
168 | c = cast(ExpectMsg, s.events[0]).find_conn(runner)
169 | if conn is None:
170 | conn = c
171 | elif c != conn:
172 | raise SpecFileError(self, "sequences do not all use the same conn?")
173 | assert conn
174 |
175 | all_done = True
176 | sequences = self.enabled_sequences(runner)
177 | while sequences != []:
178 | # Get message
179 | binmsg = runner.get_output_message(conn, sequences[0].events[0])
180 | if binmsg is None:
181 | raise EventError(
182 | self,
183 | "Did not receive a message from runner, still expecting {}".format(
184 | [s.events[0] for s in sequences]
185 | ),
186 | )
187 |
188 | try:
189 | msg = Message.read(namespace(), io.BytesIO(binmsg))
190 | except ValueError as ve:
191 | raise EventError(self, "Invalid msg {}: {}".format(binmsg.hex(), ve))
192 |
193 | ignored = Sequence.ignored_by_all(msg, self.enabled_sequences(runner))
194 | # If they gave us responses, send those now.
195 | if ignored is not None:
196 | for msg in ignored:
197 | binm = io.BytesIO()
198 | msg.write(binm)
199 | runner.recv(self, conn, binm.getvalue())
200 | continue
201 |
202 | seq = Sequence.match_which_sequence(runner, msg, sequences)
203 | if seq is not None:
204 | sequences.remove(seq)
205 | all_done &= seq.action(runner, skip_first=True)
206 | continue
207 |
208 | raise EventError(
209 | self,
210 | "Message did not match any sequences {}: {}".format(
211 | [s.events[0] for s in sequences], msg.to_str()
212 | ),
213 | )
214 | return all_done
215 |
216 |
217 | class TryAll(Event):
218 | """Event representing multiple sequences, each of which should be tested"""
219 |
220 | def __init__(self, *args: SequenceUnion):
221 | super().__init__()
222 | self.sequences = [Sequence(s) for s in args]
223 | self.done = [False] * len(self.sequences)
224 |
225 | def action(self, runner: "Runner") -> bool:
226 | super().action(runner)
227 |
228 | # Take first undone one, or if that fails, first enabled one.
229 | first_enabled = None
230 | first_undone = None
231 | all_done = True
232 | for i, s in enumerate(self.sequences):
233 | if not s.enabled(runner):
234 | continue
235 | if not first_enabled:
236 | first_enabled = s
237 | if self.done[i]:
238 | continue
239 | if not first_undone:
240 | first_undone = s
241 | self.done[i] = True
242 | else:
243 | all_done = False
244 |
245 | # Note: they might *all* be disabled!
246 | if first_undone:
247 | first_undone.action(runner)
248 | elif first_enabled:
249 | first_enabled.action(runner)
250 |
251 | return all_done
252 |
253 |
254 | def test_empty_sequence() -> None:
255 | class nullrunner(object):
256 | class dummyconfig(object):
257 | def getoption(self, name: str) -> bool:
258 | return False
259 |
260 | def __init__(self) -> None:
261 | self.config = self.dummyconfig()
262 |
263 | # This sequence should be tried twice.
264 | seq = Sequence(TryAll([], []))
265 | assert seq.action(nullrunner()) is False # type: ignore
266 | assert seq.action(nullrunner()) is True # type: ignore
267 |
--------------------------------------------------------------------------------
/lnprototest/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from .ln_spec_utils import (
2 | LightningUtils,
3 | connect_to_node_helper,
4 | open_and_announce_channel_helper,
5 | )
6 | from .utils import (
7 | Side,
8 | privkey_expand,
9 | wait_for,
10 | check_hex,
11 | gen_random_keyset,
12 | run_runner,
13 | pubkey_of,
14 | check_hex,
15 | privkey_for_index,
16 | merge_events_sequences,
17 | )
18 | from .bitcoin_utils import (
19 | ScriptType,
20 | BitcoinUtils,
21 | utxo,
22 | utxo_amount,
23 | funding_amount_for_utxo,
24 | tx_spendable,
25 | tx_out_for_index,
26 | )
27 |
--------------------------------------------------------------------------------
/lnprototest/utils/bitcoin_utils.py:
--------------------------------------------------------------------------------
1 | """
2 | Bitcoin utils is a collection of methods that helps to
3 | work with bitcoin primitive.
4 | """
5 |
6 | import hashlib
7 |
8 | from enum import Enum
9 | from typing import Tuple
10 |
11 | import bitcoin.core
12 |
13 | from bitcoin.core import Hash160, x
14 | from bitcoin.core.script import OP_0, OP_CHECKSIG, CScript
15 | from bitcoin.wallet import CBitcoinSecret
16 |
17 |
18 | class ScriptType(Enum):
19 | """
20 | Type of Script used in the Runner.
21 |
22 | In particular, during the testing we need to have
23 | two type of script, the valid one and the invalid one.
24 | This is useful when is needed to send an invalid script.
25 |
26 | FIXME: naming is too simple.
27 | """
28 |
29 | VALID_CLOSE_SCRIPT = 1
30 | INVALID_CLOSE_SCRIPT = 2
31 |
32 |
33 | class BitcoinUtils:
34 | """Main implementation class of the lightning networks utils.
35 |
36 | The implementation class contains only static methods that
37 | apply the rules specified by the BIP."""
38 |
39 | @staticmethod
40 | def blockchain_hash() -> str:
41 | """Return the chain transaction hash.
42 | That in this case is the regtest transaction hash."""
43 | return "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"
44 |
45 | @staticmethod
46 | def build_valid_script(
47 | script_type: ScriptType = ScriptType.VALID_CLOSE_SCRIPT,
48 | word: str = "lnprototest",
49 | ) -> str:
50 | """Build a valid bitcoin script and hide the primitive of the library"""
51 | secret_str = f"correct horse battery staple {word}"
52 | h = hashlib.sha256(secret_str.encode("ascii")).digest()
53 | seckey = CBitcoinSecret.from_secret_bytes(h)
54 | if script_type is ScriptType.VALID_CLOSE_SCRIPT:
55 | return CScript([OP_0, Hash160(seckey.pub)]).hex()
56 | elif script_type is ScriptType.INVALID_CLOSE_SCRIPT:
57 | return CScript([seckey.pub, OP_CHECKSIG]).hex()
58 |
59 | @staticmethod
60 | def build_script(hex: str) -> CScript:
61 | return CScript(x(hex))
62 |
63 |
64 | # Here are the keys to spend funds, derived from BIP32 seed
65 | # `0000000000000000000000000000000000000000000000000000000000000001`:
66 | #
67 | # pubkey 0/0/1: 02d6a3c2d0cf7904ab6af54d7c959435a452b24a63194e1c4e7c337d3ebbb3017b
68 | # privkey 0/0/1: 76edf0c303b9e692da9cb491abedef46ca5b81d32f102eb4648461b239cb0f99
69 | # WIF 0/0/1: cRZtHFwyrV3CS1Muc9k4sXQRDhqA1Usgi8r7NhdEXLgM5CUEZufg
70 | # P2WPKH 0/0/1: bcrt1qsdzqt93xsyewdjvagndw9523m27e52er5ca7hm
71 | # UTXO: d3fb780146954eb42e371c80cbee1725f8ae330848522f105bda24e1fb1fc010/1 (0.01BTC)
72 | #
73 | # pubkey 0/0/2: 038f1573b4238a986470d250ce87c7a91257b6ba3baf2a0b14380c4e1e532c209d
74 | # privkey 0/0/2: bc2f48a76a6b8815940accaf01981d3b6347a68fbe844f81c50ecbadf27cd179
75 | # WIF 0/0/2: cTtWRYC39drNzaANPzDrgoYsMgs5LkfE5USKH9Kr9ySpEEdjYt3E
76 | # P2WPKH 0/0/2: bcrt1qlkt93775wmf33uacykc49v2j4tayn0yj25msjn
77 | # UTXO: d3fb780146954eb42e371c80cbee1725f8ae330848522f105bda24e1fb1fc010/0 (0.02BTC)
78 | #
79 | # pubkey 0/0/3: 02ffef0c295cf7ca3a4ceb8208534e61edf44c606e7990287f389f1ea055a1231c
80 | # privkey 0/0/3: 16c5027616e940d1e72b4c172557b3b799a93c0582f924441174ea556aadd01c
81 | # WIF 0/0/3: cNLxnoJSQDRzXnGPr4ihhy2oQqRBTjdUAM23fHLHbZ2pBsNbqMwb
82 | # P2WPKH 0/0/3: bcrt1q2ng546gs0ylfxrvwx0fauzcvhuz655en4kwe2c
83 | # UTXO: d3fb780146954eb42e371c80cbee1725f8ae330848522f105bda24e1fb1fc010/3 (0.03BTC)
84 | #
85 | # pubkey 0/0/4: 026957e53b46df017bd6460681d068e1d23a7b027de398272d0b15f59b78d060a9
86 | # privkey 0/0/4: 53ac43309b75d9b86bef32c5bbc99c500910b64f9ae089667c870c2cc69e17a4
87 | # WIF 0/0/4: cQPMJRjxse9i1jDeCo8H3khUMHYfXYomKbwF5zUqdPrFT6AmtTbd
88 | # P2WPKH 0/0/4: bcrt1qrdpwrlrmrnvn535l5eldt64lxm8r2nwkv0ruxq
89 | # UTXO: d3fb780146954eb42e371c80cbee1725f8ae330848522f105bda24e1fb1fc010/4 (0.04BTC)
90 | #
91 | # pubkey 0/0/5: 03a9f795ff2e4c27091f40e8f8277301824d1c3dfa6b0204aa92347314e41b1033
92 | # privkey 0/0/5: 16be98a5d4156f6f3af99205e9bc1395397bca53db967e50427583c94271d27f
93 | # WIF 0/0/5: cNLuxyjvR6ga2q6fdmSKxAd1CPQDShKV9yoA7zFKT7GJwZXr9MmT
94 | # P2WPKH 0/0/5: bcrt1q622lwmdzxxterumd746eu3d3t40pq53p62zhlz
95 | # UTXO: d3fb780146954eb42e371c80cbee1725f8ae330848522f105bda24e1fb1fc010/2 (48.89994700BTC)
96 | #
97 | #
98 | # We add another UTXO which is solely spendable by the test framework, and not accessible to the
99 | # runner -- needed for dual-funded tests.
100 | #
101 | # pubkey: 02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5
102 | # privkey: 0000000000000000000000000000000000000000000000000000000000000002
103 | # P2WPKH : bcrt1qq6hag67dl53wl99vzg42z8eyzfz2xlkvwk6f7m
104 | # UTXO: d3fb780146954eb42e371c80cbee1725f8ae330848522f105bda24e1fb1fc010/5 (0.005BTC)
105 | #
106 | # UTXO with a 1-of-5 multisig (results in a long/expensive witness)
107 | # -- needed for dual-funded tests.
108 | #
109 | # pubkey_1: 0253cdf835e328346a4f19de099cf3d42d4a7041e073cd4057a1c4fd7cdbb1228f
110 | # privkey_1: cPToYwmZxXaAaAw6XHi8aEVefYAoYxPs8TCFpsAF6JVWbg1NSaZE
111 | # pubkey_2: 03ae903722f21f85e651b8f9b18fc854084fb90eeb76452bdcfd0cb43a16a382a2
112 | # privkey_2: cMsCoM96my8y5DDHLJfpGzFo7EhnhNFscbARtNS8mz7vTV5PiVR3
113 | # pubkey_3: 036c264d68a9727afdc75949f7d7fa71910ae9ae8001a1fbffa6f7ce000976597c
114 | # privkey_3: cQE1RbQyGcecfzTurwayy9vJtapRmM9isUG6AVTxYf2seiBHcNMp
115 | # pubkey_4: 036429fa8a4ef0b2b1d5cb553e34eeb90a32ab19fae1f0024f332ab4f74283a728
116 | # privkey_4: cVKnBoJ294xQyzRWFkxSmxMr1PFdHxGgTVBrMWpSF6o8KoJhWbNY
117 | # pubkey_5: 03d4232f19ea85051e7b76bf5f01d03e17eea8751463dee36d71413a739de1a927
118 | # privkey_5: cRboxysVFZUxZQ3DdvoTRuBKfStQ46MAA5HTVgVQW6VKF88HrC4H
119 | # script: 51210253cdf835e328346a4f19de099cf3d42d4a7041e073cd4057a1c4fd7cdbb1228f2103ae903722f21f85e651b8f9b18fc854084fb90eeb76452bdcfd0cb43a16a382a221036c264d68a9727afdc75949f7d7fa71910ae9ae8001a1fbffa6f7ce000976597c21036429fa8a4ef0b2b1d5cb553e34eeb90a32ab19fae1f0024f332ab4f74283a7282103d4232f19ea85051e7b76bf5f01d03e17eea8751463dee36d71413a739de1a92755ae
120 | # P2WSH: bcrt1qug62lyrfd7khs7welgu28y66zzuq5nc4t9gdnyx3rjm9fud2f7gqm0ksxn
121 | # UTXO: d3fb780146954eb42e371c80cbee1725f8ae330848522f105bda24e1fb1fc010/6 (0.06BTC)
122 | #
123 | tx_spendable = "0200000000010184591a56720aabc8023cecf71801c5e0f9d049d0c550ab42412ad12a67d89f3a0000000000feffffff0780841e0000000000160014fd9658fbd476d318f3b825b152b152aafa49bc9240420f000000000016001483440596268132e6c99d44dae2d151dabd9a2b232c180a2901000000160014d295f76da2319791f36df5759e45b15d5e105221c0c62d000000000016001454d14ae910793e930d8e33d3de0b0cbf05aa533300093d00000000001600141b42e1fc7b1cd93a469fa67ed5eabf36ce354dd620a107000000000016001406afd46bcdfd22ef94ac122aa11f241244a37ecc808d5b000000000022002000b068df6e0e0542e776cea5ebe8f5f1a9b40b531ddd8e94b1a7ff9829b5bbaa024730440220367b9bfed0565bad2137124f736373626fa3135e59b20a7b5c1d8f2b8f1b26bb02202f664de39787082a376d222487f02ef19e45696c041044a6d579eecabb68e94501210356609a904a7026c7391d3fbf71ad92a00e04b4cd2fb6a8d1e69cbc0998f6690a65000000"
124 |
125 |
126 | def utxo(index: int = 0) -> Tuple[str, int, int, str, int]:
127 | """Helper to get a P2WPKH UTXO, amount, privkey and fee from the tx_spendable transaction"""
128 |
129 | amount = (index + 1) * 1000000
130 | if index == 0:
131 | txout = 1
132 | key = "76edf0c303b9e692da9cb491abedef46ca5b81d32f102eb4648461b239cb0f99"
133 | elif index == 1:
134 | txout = 0
135 | key = "bc2f48a76a6b8815940accaf01981d3b6347a68fbe844f81c50ecbadf27cd179"
136 | elif index == 2:
137 | txout = 3
138 | key = "16c5027616e940d1e72b4c172557b3b799a93c0582f924441174ea556aadd01c"
139 | elif index == 3:
140 | txout = 4
141 | key = "53ac43309b75d9b86bef32c5bbc99c500910b64f9ae089667c870c2cc69e17a4"
142 | elif index == 4:
143 | txout = 2
144 | key = "16be98a5d4156f6f3af99205e9bc1395397bca53db967e50427583c94271d27f"
145 | amount = 4983494700
146 | elif index == 5:
147 | txout = 5
148 | key = "0000000000000000000000000000000000000000000000000000000000000002"
149 | amount = 500000
150 | elif index == 6:
151 | txout = 6
152 | key = "38204720bc4f9647fd58c6d0a4bd3a6dd2be16d8e4273c4d1bdd5774e8c51eaf"
153 | amount = 6000000
154 | else:
155 | raise ValueError("index must be 0-6 inclusive")
156 |
157 | # Reasonable funding fee in sats
158 | reasonable_funding_fee = 200
159 |
160 | return txid_raw(tx_spendable), txout, amount, key, reasonable_funding_fee
161 |
162 |
163 | def tx_out_for_index(index: int = 0) -> int:
164 | _, txout, _, _, _ = utxo(index)
165 | return txout
166 |
167 |
168 | def utxo_amount(index: int = 0) -> int:
169 | """How much is this utxo worth"""
170 | _, _, amt, _, _ = utxo(index)
171 | return amt
172 |
173 |
174 | def funding_amount_for_utxo(index: int = 0) -> int:
175 | """How much can we fund a channel for using utxo #index?"""
176 | _, _, amt, _, fee = utxo(index)
177 | return amt - fee
178 |
179 |
180 | def txid_raw(tx: str) -> str:
181 | """Helper to get the txid of a tx: note this is in wire protocol order, not bitcoin order!"""
182 | return bitcoin.core.CTransaction.deserialize(bytes.fromhex(tx)).GetTxid().hex()
183 |
--------------------------------------------------------------------------------
/lnprototest/utils/ln_spec_utils.py:
--------------------------------------------------------------------------------
1 | """
2 | Lightning network Speck utils, is a collection of methods that helps to
3 | work with some concept of lightning network RFC
4 |
5 | It also contains a method to generate the correct sequence of channel opening
6 | and, and it feels a dictionary with all the propriety that needs to
7 | be used after this sequence of steps.
8 |
9 | author: Vincenzo PAlazzo https://github.com/vincenzopalazzo
10 | """
11 |
12 | from typing import List, Optional
13 |
14 |
15 | class LightningUtils:
16 | """
17 | Main implementation class of the lightning networks utils.
18 |
19 | The implementation class contains only static methods that
20 | apply the rules specified in the lightning network RFC.
21 | """
22 |
23 | @staticmethod
24 | def derive_short_channel_id(block_height: int, tx_idx: int, tx_output) -> str:
25 | """
26 | Derive the short channel id with the specified
27 | parameters, and return the result as string.
28 |
29 | RFC definition: https://github.com/lightning/bolts/blob/93909f67f6a48ee3f155a6224c182e612dd5f187/07-routing-gossip.md#definition-of-short_channel_id
30 |
31 | The short_channel_id is the unique description of the funding transaction. It is constructed as follows:
32 | - the most significant 3 bytes: indicating the block height
33 | - the next 3 bytes: indicating the transaction index within the block
34 | - the least significant 2 bytes: indicating the output index that pays to the channel.
35 |
36 | e.g: a short_channel_id might be written as 539268x845x1, indicating a channel on the
37 | output 1 of the transaction at index 845 of the block at height 539268.
38 |
39 | block_height: str
40 | Block height.
41 | tx_idx: int
42 | Transaction index inside the block.
43 | tx_output: int
44 | Output index inside the transaction.
45 | """
46 | return f"{block_height}x{tx_idx}x{tx_output}"
47 |
48 |
49 | def connect_to_node_helper(
50 | runner: "Runner",
51 | tx_spendable: str,
52 | conn_privkey: str = "02",
53 | global_features: Optional[List[int]] = None,
54 | features: Optional[List[int]] = None,
55 | ) -> List["Event"]:
56 | """Helper function to make a connection with the node"""
57 | from lnprototest.utils.bitcoin_utils import tx_spendable
58 | from lnprototest import (
59 | Connect,
60 | Block,
61 | ExpectMsg,
62 | Msg,
63 | )
64 |
65 | return [
66 | Block(blockheight=102, txs=[tx_spendable]),
67 | Connect(connprivkey=conn_privkey),
68 | ExpectMsg("init"),
69 | Msg(
70 | "init",
71 | globalfeatures=(
72 | runner.runner_features(globals=True)
73 | if global_features is None
74 | else (
75 | runner.runner_features(
76 | global_features,
77 | globals=True,
78 | )
79 | )
80 | ),
81 | features=(
82 | runner.runner_features()
83 | if features is None
84 | else (runner.runner_features(features))
85 | ),
86 | ),
87 | ]
88 |
89 |
90 | def open_and_announce_channel_helper(
91 | runner: "Runner", conn_privkey: str = "02", opts: dict = {}
92 | ) -> List["Event"]:
93 | from lnprototest.utils import gen_random_keyset, pubkey_of
94 | from lnprototest.utils.bitcoin_utils import (
95 | BitcoinUtils,
96 | utxo,
97 | funding_amount_for_utxo,
98 | )
99 | from lnprototest.utils.ln_spec_utils import LightningUtils
100 | from lnprototest import (
101 | Block,
102 | ExpectMsg,
103 | Msg,
104 | Commit,
105 | Side,
106 | CreateFunding,
107 | remote_funding_pubkey,
108 | remote_revocation_basepoint,
109 | remote_payment_basepoint,
110 | remote_delayed_payment_basepoint,
111 | remote_htlc_basepoint,
112 | remote_per_commitment_point,
113 | remote_funding_privkey,
114 | msat,
115 | )
116 | from lnprototest.stash import (
117 | rcvd,
118 | funding,
119 | sent,
120 | commitsig_to_recv,
121 | channel_id,
122 | commitsig_to_send,
123 | funding_txid,
124 | funding_tx,
125 | stash_field_from_event,
126 | )
127 |
128 | # Make up a channel between nodes 02 and 03, using bitcoin privkeys 10 and 20
129 | local_keyset = gen_random_keyset()
130 | local_funding_privkey = "20"
131 | if "block_height" in opts:
132 | block_height = opts["block_height"]
133 | else:
134 | block_height = 103
135 |
136 | short_channel_id = LightningUtils.derive_short_channel_id(block_height, 1, 0)
137 | opts["short_channel_id"] = short_channel_id
138 | opts["block_height"] = block_height + 6
139 | return [
140 | Msg(
141 | "open_channel",
142 | chain_hash=BitcoinUtils.blockchain_hash(),
143 | temporary_channel_id="00" * 32,
144 | funding_satoshis=funding_amount_for_utxo(0),
145 | push_msat=0,
146 | dust_limit_satoshis=546,
147 | max_htlc_value_in_flight_msat=4294967295,
148 | channel_reserve_satoshis=9998,
149 | htlc_minimum_msat=0,
150 | feerate_per_kw=253,
151 | # clightning uses to_self_delay=6; we use 5 to test differentiation
152 | to_self_delay=5,
153 | max_accepted_htlcs=483,
154 | funding_pubkey=pubkey_of(local_funding_privkey),
155 | revocation_basepoint=local_keyset.revocation_basepoint(),
156 | payment_basepoint=local_keyset.payment_basepoint(),
157 | delayed_payment_basepoint=local_keyset.delayed_payment_basepoint(),
158 | htlc_basepoint=local_keyset.htlc_basepoint(),
159 | first_per_commitment_point=local_keyset.per_commit_point(0),
160 | channel_flags=1,
161 | ),
162 | ExpectMsg(
163 | "accept_channel",
164 | funding_pubkey=remote_funding_pubkey(),
165 | revocation_basepoint=remote_revocation_basepoint(),
166 | payment_basepoint=remote_payment_basepoint(),
167 | delayed_payment_basepoint=remote_delayed_payment_basepoint(),
168 | htlc_basepoint=remote_htlc_basepoint(),
169 | first_per_commitment_point=remote_per_commitment_point(0),
170 | minimum_depth=stash_field_from_event("accept_channel", dummy_val=3),
171 | channel_reserve_satoshis=9998,
172 | ),
173 | # Create and stash Funding object and FundingTx
174 | CreateFunding(
175 | *utxo(0),
176 | local_node_privkey="02",
177 | local_funding_privkey=local_funding_privkey,
178 | remote_node_privkey=runner.get_node_privkey(),
179 | remote_funding_privkey=remote_funding_privkey(),
180 | ),
181 | Commit(
182 | funding=funding(),
183 | opener=Side.local,
184 | local_keyset=local_keyset,
185 | local_to_self_delay=rcvd("to_self_delay", int),
186 | remote_to_self_delay=sent("to_self_delay", int),
187 | local_amount=msat(sent("funding_satoshis", int)),
188 | remote_amount=0,
189 | local_dust_limit=546,
190 | remote_dust_limit=546,
191 | feerate=253,
192 | local_features=sent("init.features"),
193 | remote_features=rcvd("init.features"),
194 | ),
195 | Msg(
196 | "funding_created",
197 | temporary_channel_id=rcvd(),
198 | funding_txid=funding_txid(),
199 | funding_output_index=0,
200 | signature=commitsig_to_send(),
201 | ),
202 | ExpectMsg(
203 | "funding_signed", channel_id=channel_id(), signature=commitsig_to_recv()
204 | ),
205 | # Mine it and get it deep enough to confirm channel.
206 | Block(
207 | blockheight=block_height,
208 | number=stash_field_from_event(
209 | "accept_channel", field_name="minimum_depth", dummy_val=3
210 | ),
211 | txs=[funding_tx()],
212 | ),
213 | ExpectMsg(
214 | "channel_ready",
215 | channel_id=channel_id(),
216 | second_per_commitment_point=remote_per_commitment_point(1),
217 | ),
218 | Msg(
219 | "channel_ready",
220 | channel_id=channel_id(),
221 | second_per_commitment_point=local_keyset.per_commit_point(1),
222 | ),
223 | # wait confirmations
224 | #
225 | # FIXME: Uh! do you know why this sucks, because lnprototest is lazy evaluated.
226 | # This is huggly, and we should change it at some point but this now works.
227 | Block(
228 | blockheight=lambda runner, event, _: block_height
229 | + runner.get_stash(event, "accept_channel").fields["minimum_depth"],
230 | number=6,
231 | ),
232 | ]
233 |
--------------------------------------------------------------------------------
/lnprototest/utils/utils.py:
--------------------------------------------------------------------------------
1 | """
2 | Utils module that implement common function used across lnprototest library.
3 | """
4 |
5 | import string
6 | import coincurve
7 | import time
8 | import typing
9 | import logging
10 | import traceback
11 |
12 | from typing import Union, Sequence, List
13 | from enum import IntEnum
14 |
15 | from lnprototest.keyset import KeySet
16 |
17 |
18 | class Side(IntEnum):
19 | local = 0
20 | remote = 1
21 |
22 | def __not__(self) -> "Side":
23 | if self == Side.local:
24 | return Side.remote
25 | return Side.local
26 |
27 |
28 | def check_hex(val: str, digits: int) -> str:
29 | if not all(c in string.hexdigits for c in val):
30 | raise ValueError("{} is not valid hex".format(val))
31 | if len(val) != digits:
32 | raise ValueError("{} not {} characters long".format(val, digits))
33 | return val
34 |
35 |
36 | def privkey_expand(secret: str) -> coincurve.PrivateKey:
37 | # Privkey can be truncated, since we use tiny values a lot.
38 | return coincurve.PrivateKey(bytes.fromhex(secret).rjust(32, bytes(1)))
39 |
40 |
41 | def pubkey_of(privkey: str) -> str:
42 | """Return the public key corresponding to this privkey"""
43 | return (
44 | coincurve.PublicKey.from_secret(privkey_expand(privkey).secret).format().hex()
45 | )
46 |
47 |
48 | def privkey_for_index(index: int = 0) -> str:
49 | from lnprototest.utils.bitcoin_utils import utxo
50 |
51 | _, _, _, privkey, _ = utxo(index)
52 | return privkey
53 |
54 |
55 | def gen_random_keyset(counter: int = 20) -> KeySet:
56 | """Helper function to generate a random keyset."""
57 |
58 | from lnprototest import privkey_expand
59 |
60 | return KeySet(
61 | revocation_base_secret=f"{counter + 1}",
62 | payment_base_secret=f"{counter + 2}",
63 | htlc_base_secret=f"{counter + 3}",
64 | delayed_payment_base_secret=f"{counter + 4}",
65 | shachain_seed="00" * 32,
66 | )
67 |
68 |
69 | def wait_for(success: typing.Callable, timeout: int = 180) -> None:
70 | start_time = time.time()
71 | interval = 0.25
72 | while not success():
73 | time_left = start_time + timeout - time.time()
74 | if time_left <= 0:
75 | raise ValueError("Timeout while waiting for {}", success)
76 | time.sleep(min(interval, time_left))
77 | interval *= 2
78 | if interval > 5:
79 | interval = 5
80 |
81 |
82 | def get_traceback(e: Exception) -> str:
83 | lines = traceback.format_exception(type(e), e, e.__traceback__)
84 | return "".join(lines)
85 |
86 |
87 | def run_runner(runner: "Runner", test: Union[Sequence, List["Event"], "Event"]) -> None:
88 | """
89 | The pytest using the assertion as safe failure, and the exception it is only
90 | an event that must not happen.
91 |
92 | From design, lnprototest fails with an exception, and for this reason, if the
93 | lnprototest throws an exception, we catch it, and we fail with an assent.
94 | """
95 | try:
96 | runner.run(test)
97 | except Exception as ex:
98 | runner.stop(print_logs=True)
99 | logging.error(get_traceback(ex))
100 | assert False, ex
101 |
102 |
103 | def merge_events_sequences(
104 | pre: Union[Sequence, List["Event"], "Event"],
105 | post: Union[Sequence, List["Event"], "Event"],
106 | ) -> Union[Sequence, List["Event"], "Event"]:
107 | """Merge the two list in the pre-post order"""
108 | pre.extend(post)
109 | return pre
110 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "lnprototest"
3 | version = "0.0.7"
4 | description = "Spec protocol tests for lightning network implementations"
5 | authors = ["Rusty Russell ", "Vincenzo Palazzo "]
6 | license = "MIT"
7 | readme = "README.md"
8 |
9 | [tool.poetry.dependencies]
10 | python = "^3.9"
11 | pyln-bolt4 = "^1.0.222"
12 | pyln-bolt2 = "^1.0.222"
13 | pyln-bolt1 = "^1.0.222"
14 | pyln-client = "^25.2.1"
15 | pyln-testing = "^25.2.1"
16 | crc32c = "^2.2.post0"
17 | # We accidentally published version 1.0.186 instead of 1.0.2.186. That
18 | # version is now yanked by caches remain, so this is a temporary fix.
19 | pyln-bolt7 = "^1.0.246"
20 | pyln-proto = "^23.05.2"
21 | python-bitcoinlib = "^0.11.2"
22 |
23 | [tool.poetry.group.dev.dependencies]
24 | pytest = "^7.2.1"
25 | black = "^25.1.0"
26 | flake8 = "^4.0.1"
27 | pytest-xdist = "^3.1.0"
28 | mypy = "^1.15.0"
29 |
30 | [build-system]
31 | requires = ["poetry-core>=1.0.0"]
32 | build-backend = "poetry.core.masonry.api"
33 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/python3
2 | import pytest
3 | import importlib
4 | import lnprototest
5 | import pyln.spec.bolt1
6 | import pyln.spec.bolt2
7 | import pyln.spec.bolt7
8 | from pyln.proto.message import MessageNamespace
9 | from typing import Any, Callable, Generator, List
10 |
11 |
12 | def pytest_addoption(parser: Any) -> None:
13 | parser.addoption(
14 | "--runner",
15 | action="store",
16 | help="runner to use",
17 | default="lnprototest.DummyRunner",
18 | )
19 | parser.addoption(
20 | "--runner-args",
21 | action="append",
22 | help="parameters for runner to use",
23 | default=[],
24 | )
25 |
26 |
27 | @pytest.fixture() # type: ignore
28 | def runner(pytestconfig: Any) -> Any:
29 | parts = pytestconfig.getoption("runner").rpartition(".")
30 | runner = importlib.import_module(parts[0]).__dict__[parts[2]](pytestconfig)
31 | yield runner
32 | runner.teardown()
33 |
34 |
35 | @pytest.fixture()
36 | def namespaceoverride(
37 | pytestconfig: Any,
38 | ) -> Generator[Callable[[MessageNamespace], None], None, None]:
39 | """Use this if you want to override the message namespace"""
40 |
41 | def _setter(newns: MessageNamespace) -> None:
42 | lnprototest.assign_namespace(newns)
43 |
44 | yield _setter
45 | # Restore it
46 | lnprototest.assign_namespace(lnprototest.peer_message_namespace())
47 |
48 |
49 | @pytest.fixture()
50 | def with_proposal(
51 | pytestconfig: Any,
52 | ) -> Generator[Callable[[List[str]], None], None, None]:
53 | """Use this to add additional messages to the namespace
54 | Useful for testing proposed (but not yet merged) spec mods. Noop if it seems already merged.
55 | """
56 |
57 | def _setter(proposal_csv: List[str]) -> None:
58 | # Testing first line is cheap, pretty effective.
59 | if proposal_csv[0] not in (
60 | pyln.spec.bolt1.csv + pyln.spec.bolt2.csv + pyln.spec.bolt7.csv
61 | ):
62 | # We merge *csv*, because then you can add tlv entries; merging
63 | # namespaces with duplicate TLVs complains of a clash.
64 | lnprototest.assign_namespace(
65 | lnprototest.make_namespace(
66 | pyln.spec.bolt1.csv
67 | + pyln.spec.bolt2.csv
68 | + pyln.spec.bolt7.csv
69 | + proposal_csv
70 | )
71 | )
72 |
73 | yield _setter
74 |
75 | # Restore it
76 | lnprototest.assign_namespace(lnprototest.peer_message_namespace())
77 |
--------------------------------------------------------------------------------
/tests/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = --strict-markers
3 | xfail_strict = True
--------------------------------------------------------------------------------
/tests/test_bolt1-01-init.py:
--------------------------------------------------------------------------------
1 | """
2 | Variation of init exchange.
3 |
4 | Spec: MUST respond to known feature bits as specified in [BOLT #9](09-features.md).
5 | """
6 |
7 | import functools
8 |
9 | from typing import List, Any
10 |
11 | import pytest
12 | import pyln.spec.bolt1
13 |
14 | from pyln.proto.message import Message
15 |
16 | from lnprototest.stash import rcvd
17 | from lnprototest.utils.utils import run_runner
18 | from lnprototest import (
19 | Runner,
20 | Event,
21 | Sequence,
22 | TryAll,
23 | Connect,
24 | Disconnect,
25 | EventError,
26 | ExpectMsg,
27 | Msg,
28 | has_bit,
29 | bitfield,
30 | bitfield_len,
31 | SpecFileError,
32 | ExpectDisconnect,
33 | )
34 |
35 |
36 | # BOLT #1: The sending node:
37 | # ...
38 | # - SHOULD NOT set features greater than 13 in `globalfeatures`.
39 | def no_gf13(event: Event, msg: Message, runner: "Runner") -> None:
40 | for i in range(14, bitfield_len(msg.fields["globalfeatures"])):
41 | if has_bit(msg.fields["globalfeatures"], i):
42 | raise EventError(event, "globalfeatures bit {} set".format(i))
43 |
44 |
45 | def no_feature(
46 | featurebits: List[int], event: Event, msg: Message, runner: "Runner"
47 | ) -> None:
48 | for bit in featurebits:
49 | if has_bit(msg.fields["features"], bit):
50 | raise EventError(
51 | event, "features set bit {} unexpected: {}".format(bit, msg.to_str())
52 | )
53 |
54 |
55 | def has_feature(
56 | featurebits: List[int], event: Event, msg: Message, runner: "Runner"
57 | ) -> None:
58 | for bit in featurebits:
59 | if not has_bit(msg.fields["features"], bit):
60 | raise EventError(
61 | event, "features set bit {} unset: {}".format(bit, msg.to_str())
62 | )
63 |
64 |
65 | def has_one_feature(
66 | featurebits: List[int], event: Event, msg: Message, runner: "Runner"
67 | ) -> None:
68 | has_any = False
69 | for bit in featurebits:
70 | if has_bit(msg.fields["features"], bit):
71 | has_any = True
72 |
73 | if not has_any:
74 | raise EventError(event, "none of {} set: {}".format(featurebits, msg.to_str()))
75 |
76 |
77 | def test_namespace_override(runner: Runner, namespaceoverride: Any) -> None:
78 | # Truncate the namespace to just BOLT1
79 | namespaceoverride(pyln.spec.bolt1.namespace)
80 |
81 | # Try to send a message that's not in BOLT1
82 | with pytest.raises(SpecFileError, match=r"Unknown msgtype open_channel"):
83 | Msg("open_channel")
84 |
85 |
86 | def test_echo_init(runner: Runner, namespaceoverride: Any) -> None:
87 | # We override default namespace since we only need BOLT1
88 | namespaceoverride(pyln.spec.bolt1.namespace)
89 | test = [
90 | Connect(connprivkey="03"),
91 | ExpectMsg("init"),
92 | Msg(
93 | "init",
94 | globalfeatures=runner.runner_features(globals=True),
95 | features=runner.runner_features(),
96 | ),
97 | # optionally disconnect that first one
98 | Connect(connprivkey="02"),
99 | # You should always handle us echoing your own features back!
100 | ExpectMsg("init"),
101 | Msg("init", globalfeatures=rcvd(), features=rcvd()),
102 | ]
103 |
104 | run_runner(runner, test)
105 |
106 |
107 | def test_echo_init_after_disconnect(runner: Runner, namespaceoverride: Any) -> None:
108 | # We override default namespace since we only need BOLT1
109 | namespaceoverride(pyln.spec.bolt1.namespace)
110 | test = [
111 | Connect(connprivkey="03"),
112 | ExpectMsg("init"),
113 | Msg(
114 | "init",
115 | globalfeatures=runner.runner_features(globals=True),
116 | features=runner.runner_features(),
117 | ),
118 | # optionally disconnect that first one
119 | Disconnect(),
120 | Connect(connprivkey="02"),
121 | # You should always handle us echoing your own features back!
122 | ExpectMsg("init"),
123 | Msg("init", globalfeatures=rcvd(), features=rcvd()),
124 | ]
125 |
126 | run_runner(runner, test)
127 |
128 |
129 | def test_init_check_received_msg(runner: Runner, namespaceoverride: Any) -> None:
130 | """TODO add comments"""
131 | namespaceoverride(pyln.spec.bolt1.namespace)
132 | sequences = [
133 | Connect(connprivkey="03"),
134 | ExpectMsg("init"),
135 | Msg(
136 | "init",
137 | globalfeatures=runner.runner_features(globals=True),
138 | features=runner.runner_features(),
139 | ),
140 | # optionally disconnect that first one
141 | TryAll([], Disconnect()),
142 | Connect(connprivkey="02"),
143 | # Even if we don't send anything, it should send init.
144 | ExpectMsg("init", if_match=no_gf13),
145 | ]
146 | run_runner(runner, sequences)
147 |
148 |
149 | def test_init_invalid_globalfeatures(runner: Runner, namespaceoverride: Any) -> None:
150 | """TODO add comments"""
151 | namespaceoverride(pyln.spec.bolt1.namespace)
152 | sequences = [
153 | Connect(connprivkey="03"),
154 | ExpectMsg("init"),
155 | Msg(
156 | "init",
157 | globalfeatures=runner.runner_features(globals=True),
158 | features=runner.runner_features(),
159 | ),
160 | # optionally disconnect that first one
161 | TryAll([], Disconnect()),
162 | Connect(connprivkey="02"),
163 | ExpectMsg("init", if_match=no_gf13),
164 | # BOLT #1:
165 | # The sending node:...
166 | # - SHOULD NOT set features greater than 13 in `globalfeatures`.
167 | Msg(
168 | "init",
169 | globalfeatures=runner.runner_features(
170 | globals=True, additional_features=[99]
171 | ),
172 | features=runner.runner_features(),
173 | ),
174 | ]
175 | run_runner(runner, sequences)
176 |
177 |
178 | def test_init_is_first_msg(runner: Runner, namespaceoverride: Any) -> None:
179 | """TODO add comments"""
180 | namespaceoverride(pyln.spec.bolt1.namespace)
181 | sequences = [
182 | Connect(connprivkey="03"),
183 | ExpectMsg("init"),
184 | Msg(
185 | "init",
186 | globalfeatures=runner.runner_features(globals=True),
187 | features=runner.runner_features(),
188 | ),
189 | # optionally disconnect that first one
190 | TryAll([], Disconnect()),
191 | Connect(connprivkey="02"),
192 | # Minimal possible init message.
193 | # BOLT #1:
194 | # The sending node:
195 | # - MUST send `init` as the first Lightning message for any connection.
196 | ExpectMsg("init"),
197 | Msg(
198 | "init",
199 | globalfeatures=runner.runner_features(globals=True),
200 | features=runner.runner_features(),
201 | ),
202 | ]
203 | run_runner(runner, sequences)
204 |
205 |
206 | def test_init_check_free_featurebits(runner: Runner, namespaceoverride: Any) -> None:
207 | """Sanity check that bits 98 and 99 are not used!"""
208 | namespaceoverride(pyln.spec.bolt1.namespace)
209 | sequences = [
210 | Connect(connprivkey="03"),
211 | ExpectMsg("init"),
212 | Msg(
213 | "init",
214 | globalfeatures=runner.runner_features(globals=True),
215 | features=runner.runner_features(),
216 | ),
217 | # optionally disconnect that first one
218 | TryAll([], Disconnect()),
219 | Connect(connprivkey="02"),
220 | ExpectMsg("init", if_match=functools.partial(no_feature, [98, 99])),
221 | # BOLT #1:
222 | # The receiving node:...
223 | # - upon receiving unknown _odd_ feature bits that are non-zero:
224 | # - MUST ignore the bit.
225 | # init msg with unknown odd local bit (99): no error
226 | Msg(
227 | "init",
228 | globalfeatures=runner.runner_features(globals=True),
229 | features=runner.runner_features(additional_features=[99]),
230 | ),
231 | ]
232 | run_runner(runner, sequences)
233 |
234 |
235 | def test_init_fail_connection_if_receive_an_even_unknown_featurebits(
236 | runner: Runner, namespaceoverride: Any
237 | ) -> None:
238 | """Sanity check that bits 98 and 99 are not used!"""
239 | namespaceoverride(pyln.spec.bolt1.namespace)
240 | sequences = [
241 | Connect(connprivkey="03"),
242 | ExpectMsg("init"),
243 | Msg(
244 | "init",
245 | globalfeatures=runner.runner_features(globals=True),
246 | features=runner.runner_features(),
247 | ),
248 | # optionally disconnect that first one
249 | TryAll([], Disconnect()),
250 | Connect(connprivkey="02"),
251 | # BOLT #1:
252 | # The receiving node: ...
253 | # - upon receiving unknown _even_ feature bits that are non-zero:
254 | # - MUST fail the connection.
255 | ExpectMsg("init"),
256 | Msg(
257 | "init",
258 | globalfeatures=runner.runner_features(globals=True),
259 | features=runner.runner_features(additional_features=[98]),
260 | ),
261 | ExpectDisconnect(),
262 | ]
263 | run_runner(runner, sequences)
264 |
265 |
266 | def test_init_fail_connection_if_receive_an_even_unknown_globalfeaturebits(
267 | runner: Runner, namespaceoverride: Any
268 | ) -> None:
269 | """Sanity check that bits 98 and 99 are not used!"""
270 | namespaceoverride(pyln.spec.bolt1.namespace)
271 | sequences = [
272 | Connect(connprivkey="03"),
273 | ExpectMsg("init"),
274 | Msg(
275 | "init",
276 | globalfeatures=runner.runner_features(globals=True),
277 | features=runner.runner_features(),
278 | ),
279 | # optionally disconnect that first one
280 | TryAll([], Disconnect()),
281 | Connect(connprivkey="02"),
282 | # init msg with unknown even global bit (98): you will error
283 | ExpectMsg("init"),
284 | Msg(
285 | "init",
286 | globalfeatures=runner.runner_features(
287 | globals=True, additional_features=[98]
288 | ),
289 | features=runner.runner_features(),
290 | ),
291 | ExpectDisconnect(),
292 | ]
293 | run_runner(runner, sequences)
294 |
295 |
296 | def test_init_fail_ask_for_option_data_loss_protect(
297 | runner: Runner, namespaceoverride: Any
298 | ) -> None:
299 | """Sanity check that bits 98 and 99 are not used!"""
300 | namespaceoverride(pyln.spec.bolt1.namespace)
301 | sequences = [
302 | Connect(connprivkey="03"),
303 | ExpectMsg("init"),
304 | Msg(
305 | "init",
306 | globalfeatures=runner.runner_features(globals=True),
307 | features=runner.runner_features(),
308 | ),
309 | # optionally disconnect that first one
310 | TryAll([], Disconnect()),
311 | Connect(connprivkey="02"),
312 | # If you don't support `option_data_loss_protect`, you will be ok if
313 | # we ask for it.
314 | Sequence(
315 | [
316 | ExpectMsg("init", if_match=functools.partial(no_feature, [0, 1])),
317 | Msg(
318 | "init",
319 | globalfeatures=runner.runner_features(globals=True),
320 | features=runner.runner_features(additional_features=[1]),
321 | ),
322 | ],
323 | enable=not runner.has_option("option_data_loss_protect"),
324 | ),
325 | ]
326 | run_runner(runner, sequences)
327 |
328 |
329 | def test_init_advertize_option_data_loss_protect(
330 | runner: Runner, namespaceoverride: Any
331 | ) -> None:
332 | """TODO"""
333 | namespaceoverride(pyln.spec.bolt1.namespace)
334 | sequences = [
335 | Connect(connprivkey="03"),
336 | ExpectMsg("init"),
337 | Msg(
338 | "init",
339 | globalfeatures=runner.runner_features(globals=True),
340 | features=runner.runner_features(),
341 | ),
342 | # optionally disconnect that first one
343 | TryAll([], Disconnect()),
344 | Connect(connprivkey="02"),
345 | # If you support `option_data_loss_protect`, you will advertize it odd.
346 | Sequence(
347 | [ExpectMsg("init", if_match=functools.partial(has_feature, [1]))],
348 | enable=(runner.has_option("option_data_loss_protect") == "odd"),
349 | ),
350 | ]
351 | run_runner(runner, sequences)
352 |
353 |
354 | def test_init_required_option_data_loss_protect(
355 | runner: Runner, namespaceoverride: Any
356 | ) -> None:
357 | """TODO"""
358 | namespaceoverride(pyln.spec.bolt1.namespace)
359 | sequences = [
360 | Connect(connprivkey="03"),
361 | ExpectMsg("init"),
362 | Msg(
363 | "init",
364 | globalfeatures=runner.runner_features(globals=True),
365 | features=runner.runner_features(),
366 | ),
367 | # optionally disconnect that first one
368 | TryAll([], Disconnect()),
369 | Connect(connprivkey="02"),
370 | # If you require `option_data_loss_protect`, you will advertize it even.
371 | Sequence(
372 | [ExpectMsg("init", if_match=functools.partial(has_feature, [0]))],
373 | enable=(runner.has_option("option_data_loss_protect") == "even"),
374 | ),
375 | ]
376 | run_runner(runner, sequences)
377 |
378 |
379 | def test_init_reject_option_data_loss_protect_if_not_supported(
380 | runner: Runner, namespaceoverride: Any
381 | ) -> None:
382 | """TODO"""
383 | namespaceoverride(pyln.spec.bolt1.namespace)
384 | sequences = [
385 | Connect(connprivkey="03"),
386 | ExpectMsg("init"),
387 | Msg(
388 | "init",
389 | globalfeatures=runner.runner_features(globals=True),
390 | features=runner.runner_features(),
391 | ),
392 | # optionally disconnect that first one
393 | TryAll([], Disconnect()),
394 | Connect(connprivkey="02"),
395 | # If you don't support `option_anchor_outputs`, you will error if
396 | # we require it.
397 | Sequence(
398 | [
399 | ExpectMsg("init", if_match=functools.partial(no_feature, [20, 21])),
400 | Msg("init", globalfeatures="", features=bitfield(20)),
401 | ExpectDisconnect(),
402 | ],
403 | enable=not runner.has_option("option_anchor_outputs"),
404 | ),
405 | ]
406 | run_runner(runner, sequences)
407 |
408 |
409 | def test_init_advertize_option_anchor_outputs(
410 | runner: Runner, namespaceoverride: Any
411 | ) -> None:
412 | """TODO"""
413 | namespaceoverride(pyln.spec.bolt1.namespace)
414 | sequences = [
415 | Connect(connprivkey="03"),
416 | ExpectMsg("init"),
417 | Msg(
418 | "init",
419 | globalfeatures=runner.runner_features(globals=True),
420 | features=runner.runner_features(),
421 | ),
422 | # optionally disconnect that first one
423 | TryAll([], Disconnect()),
424 | Connect(connprivkey="02"),
425 | # If you support `option_anchor_outputs`, you will advertize it odd.
426 | Sequence(
427 | [ExpectMsg("init", if_match=functools.partial(has_feature, [21]))],
428 | enable=(runner.has_option("option_anchor_outputs") == "odd"),
429 | ),
430 | ]
431 | run_runner(runner, sequences)
432 |
433 |
434 | def test_init_required_option_anchor_outputs(
435 | runner: Runner, namespaceoverride: Any
436 | ) -> None:
437 | """TODO"""
438 | namespaceoverride(pyln.spec.bolt1.namespace)
439 | sequences = [
440 | Connect(connprivkey="03"),
441 | ExpectMsg("init"),
442 | Msg(
443 | "init",
444 | globalfeatures=runner.runner_features(globals=True),
445 | features=runner.runner_features(),
446 | ),
447 | # optionally disconnect that first one
448 | TryAll([], Disconnect()),
449 | Connect(connprivkey="02"),
450 | # If you require `option_anchor_outputs`, you will advertize it even.
451 | Sequence(
452 | [ExpectMsg("init", if_match=functools.partial(has_feature, [20]))],
453 | enable=(runner.has_option("option_anchor_outputs") == "even"),
454 | ),
455 | ]
456 | run_runner(runner, sequences)
457 |
458 |
459 | def test_init_advertize_option_static_remotekey(
460 | runner: Runner, namespaceoverride: Any
461 | ) -> None:
462 | """TODO"""
463 | namespaceoverride(pyln.spec.bolt1.namespace)
464 | sequences = [
465 | Connect(connprivkey="03"),
466 | ExpectMsg("init"),
467 | Msg(
468 | "init",
469 | globalfeatures=runner.runner_features(globals=True),
470 | features=runner.runner_features(),
471 | ),
472 | # optionally disconnect that first one
473 | TryAll([], Disconnect()),
474 | Connect(connprivkey="02"),
475 | # BOLT-a12da24dd0102c170365124782b46d9710950ac1 #9:
476 | # | Bits | Name | ... | Dependencies
477 | # ...
478 | # | 12/13 | `option_static_remotekey` |
479 | # ...
480 | # | 20/21 | `option_anchor_outputs` | ... | `option_static_remotekey` |
481 | # If you support `option_anchor_outputs`, you will
482 | # advertize option_static_remotekey.
483 | Sequence(
484 | [ExpectMsg("init", if_match=functools.partial(has_one_feature, [12, 13]))],
485 | enable=(runner.has_option("option_anchor_outputs") is not None),
486 | ),
487 | ]
488 | run_runner(runner, sequences)
489 |
--------------------------------------------------------------------------------
/tests/test_bolt1-02-unknown-messages.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | # Init exchange, with unknown messages
3 | #
4 | import pyln.spec.bolt1
5 |
6 | from typing import Any
7 |
8 | from lnprototest import Connect, ExpectMsg, Msg, RawMsg, Runner
9 | from lnprototest.event import ExpectDisconnect
10 | from lnprototest.utils import run_runner
11 |
12 |
13 | def test_unknowns(runner: Runner, namespaceoverride: Any) -> None:
14 | # We override default namespace since we only need BOLT1
15 | namespaceoverride(pyln.spec.bolt1.namespace)
16 | test = [
17 | Connect(connprivkey="03"),
18 | ExpectMsg("init"),
19 | Msg(
20 | "init",
21 | globalfeatures=runner.runner_features(globals=True),
22 | features=runner.runner_features(),
23 | ),
24 | # BOLT #1:
25 | # A receiving node:
26 | # - upon receiving a message of _odd_, unknown type:
27 | # - MUST ignore the received message.
28 | RawMsg(bytes.fromhex("270F")),
29 | ]
30 | run_runner(runner, test)
31 |
32 |
33 | def test_unknowns_even_message(runner: Runner, namespaceoverride: Any) -> None:
34 | # We override default namespace since we only need BOLT1
35 | namespaceoverride(pyln.spec.bolt1.namespace)
36 | test = [
37 | Connect(connprivkey="03"),
38 | ExpectMsg("init"),
39 | Msg(
40 | "init",
41 | globalfeatures=runner.runner_features(globals=True),
42 | features=runner.runner_features(),
43 | ),
44 | # BOLT #1:
45 | # A receiving node:...
46 | # - upon receiving a message of _even_, unknown type:
47 | # - MUST close the connection.
48 | # - MAY fail the channels.
49 | RawMsg(bytes.fromhex("2710")),
50 | ExpectDisconnect(),
51 | ]
52 | run_runner(runner, test)
53 |
--------------------------------------------------------------------------------
/tests/test_bolt2-01-close_channel.py:
--------------------------------------------------------------------------------
1 | """
2 | testing bolt2 closing channel operation described in the lightning network speck
3 | https://github.com/lightning/bolts/blob/master/02-peer-protocol.md#channel-close
4 |
5 | The overview of what we test in this integration testing is described by the following
6 | figure.
7 |
8 | +-------+ +-------+
9 | | |--(1)----- shutdown ------->| |
10 | | |<-(2)----- shutdown --------| |
11 | | | | |
12 | | | | |
13 | | | | |
14 | | A | ... | B |
15 | | | | |
16 | | |--(3)-- closing_signed F1--->| |
17 | | |<-(4)-- closing_signed F2----| |
18 | | | ... | |
19 | | |--(?)-- closing_signed Fn--->| |
20 | | |<-(?)-- closing_signed Fn----| |
21 | +-------+ +-------+
22 |
23 | BOLT 2 proposal https://github.com/lightning/bolts/pull/972
24 |
25 | author: https://github.com/vincenzopalazzo
26 | """
27 |
28 | import pytest
29 |
30 | from lnprototest import (
31 | ExpectMsg,
32 | Msg,
33 | Runner,
34 | MustNotMsg,
35 | )
36 | from lnprototest.utils import run_runner, merge_events_sequences, tx_spendable
37 | from lnprototest.stash import channel_id
38 | from lnprototest.utils.ln_spec_utils import (
39 | open_and_announce_channel_helper,
40 | connect_to_node_helper,
41 | )
42 | from lnprototest.utils import BitcoinUtils, ScriptType
43 | from lnprototest.event import CloseChannel
44 |
45 |
46 | def test_close_channel_shutdown_msg_normal_case_receiver_side(runner: Runner) -> None:
47 | """Close the channel with the other peer, and check if the
48 | shutdown message works in the expected way.
49 |
50 | In particular, this test will check the receiver side.
51 |
52 | ________________________________
53 | | runner -> shutdown -> ln-node |
54 | | runner <- shutdown <- ln-node |
55 | --------------------------------
56 | """
57 | # the option that the helper method feels for us
58 | test_opts = {}
59 | pre_events_conn = connect_to_node_helper(
60 | runner=runner,
61 | tx_spendable=tx_spendable,
62 | conn_privkey="03",
63 | )
64 | pre_events = open_and_announce_channel_helper(
65 | runner, conn_privkey="03", opts=test_opts
66 | )
67 | # merge the two events
68 | pre_events = merge_events_sequences(pre_events_conn, pre_events)
69 | channel_idx = channel_id()
70 |
71 | script = BitcoinUtils.build_valid_script()
72 | test = [
73 | # runner sent shutdown message to ln implementation
74 | # BOLT 2:
75 | # - MUST NOT send an `update_add_htlc` after a shutdown.
76 | Msg(
77 | "shutdown",
78 | channel_id=channel_idx,
79 | scriptpubkey=script,
80 | ),
81 | MustNotMsg("update_add_htlc"),
82 | ExpectMsg(
83 | "shutdown", ignore=ExpectMsg.ignore_all_gossip, channel_id=channel_idx
84 | ),
85 | # TODO: including in bitcoin function the possibility to sign this values
86 | # Msg(
87 | # "closing_signed",
88 | # channel_id=channel_idx,
89 | # fee_satoshis='100',
90 | # signature="0000",
91 | # ),
92 | # ExpectMsg("closing_signed")
93 | ]
94 | run_runner(runner, merge_events_sequences(pre=pre_events, post=test))
95 |
96 |
97 | @pytest.mark.skip("working in progress")
98 | def test_close_channel_shutdown_msg_normal_case_sender_side(runner: Runner) -> None:
99 | """Close the channel with the other peer, and check if the
100 | shutdown message works in the expected way.
101 |
102 | In particular, this test will check the sender side, with a event flow like:
103 | ________________________________
104 | | ln-node -> shutdown -> runner |
105 | | ln-node <- shutdown <- runner |
106 | --------------------------------
107 | """
108 | # the option that the helper method feels for us
109 | test_opts = {}
110 | pre_events_conn = connect_to_node_helper(
111 | runner=runner,
112 | tx_spendable=tx_spendable,
113 | conn_privkey="03",
114 | )
115 | pre_events = open_and_announce_channel_helper(
116 | runner, conn_privkey="03", opts=test_opts
117 | )
118 | # merge the two events
119 | pre_events = merge_events_sequences(pre_events_conn, pre_events)
120 |
121 | short_channel_id = test_opts["short_channel_id"]
122 | test = [
123 | # runner sent shutdown message to ln implementation
124 | # BOLT 2:
125 | # - MUST NOT send an `update_add_htlc` after a shutdown.
126 | CloseChannel(channel_id=short_channel_id),
127 | MustNotMsg("update_add_htlc"),
128 | Msg("shutdown", ignore=ExpectMsg.ignore_all_gossip, channel_id=channel_id()),
129 | ExpectMsg("closing_signed"),
130 | ]
131 | run_runner(runner, merge_events_sequences(pre=pre_events, post=test))
132 |
133 |
134 | @pytest.mark.skip(reason="skipping this for now because looks like flacky on CI")
135 | def test_close_channel_shutdown_msg_wrong_script_pubkey_receiver_side(
136 | runner: Runner,
137 | ) -> None:
138 | """Test close operation from the receiver view point, in the case when
139 | the sender set a wrong script pub key not specified in the spec.
140 | ______________________________________________________
141 | | runner -> shutdown (wrong script pub key) -> ln-node |
142 | | runner <- warning msg <- ln-node |
143 | -------------------------------------------------------
144 | """
145 | # the option that the helper method feels for us
146 | test_opts = {}
147 | pre_events_conn = connect_to_node_helper(
148 | runner=runner,
149 | tx_spendable=tx_spendable,
150 | conn_privkey="03",
151 | )
152 | pre_events = open_and_announce_channel_helper(
153 | runner, conn_privkey="03", opts=test_opts
154 | )
155 | # merge the two events
156 | pre_events = merge_events_sequences(pre_events_conn, pre_events)
157 |
158 | script = BitcoinUtils.build_valid_script(ScriptType.INVALID_CLOSE_SCRIPT)
159 |
160 | test = [
161 | # runner sent shutdown message to the ln implementation
162 | Msg(
163 | "shutdown",
164 | channel_id=channel_id(),
165 | scriptpubkey=script,
166 | ),
167 | MustNotMsg("add_htlc"),
168 | MustNotMsg("shutdown"),
169 | ExpectMsg("warning", ignore=ExpectMsg.ignore_all_gossip),
170 | ]
171 | run_runner(runner, merge_events_sequences(pre=pre_events, post=test))
172 |
--------------------------------------------------------------------------------
/tests/test_bolt2-01-open_channel.py:
--------------------------------------------------------------------------------
1 | # Variations on open_channel, accepter + opener perspectives
2 | from lnprototest import (
3 | TryAll,
4 | Sequence,
5 | ExpectDisconnect,
6 | Block,
7 | FundChannel,
8 | ExpectMsg,
9 | ExpectTx,
10 | Msg,
11 | RawMsg,
12 | AcceptFunding,
13 | CreateFunding,
14 | Commit,
15 | Runner,
16 | remote_funding_pubkey,
17 | remote_revocation_basepoint,
18 | remote_payment_basepoint,
19 | remote_htlc_basepoint,
20 | remote_per_commitment_point,
21 | remote_delayed_payment_basepoint,
22 | Side,
23 | CheckEq,
24 | msat,
25 | remote_funding_privkey,
26 | bitfield,
27 | Block,
28 | )
29 | from lnprototest.stash import (
30 | sent,
31 | rcvd,
32 | commitsig_to_send,
33 | commitsig_to_recv,
34 | channel_id,
35 | funding_txid,
36 | funding_tx,
37 | funding,
38 | stash_field_from_event,
39 | )
40 | from lnprototest.utils import (
41 | utxo,
42 | BitcoinUtils,
43 | tx_spendable,
44 | run_runner,
45 | merge_events_sequences,
46 | funding_amount_for_utxo,
47 | pubkey_of,
48 | gen_random_keyset,
49 | )
50 | from lnprototest.utils.ln_spec_utils import (
51 | connect_to_node_helper,
52 | open_and_announce_channel_helper,
53 | )
54 |
55 |
56 | def test_open_channel_announce_features(runner: Runner) -> None:
57 | """Check that the announce features works correctly"""
58 | connections_events = connect_to_node_helper(
59 | runner=runner, tx_spendable=tx_spendable, conn_privkey="02"
60 | )
61 |
62 | test_events = [
63 | TryAll(
64 | # BOLT-a12da24dd0102c170365124782b46d9710950ac1 #9:
65 | # | 20/21 | `option_anchor_outputs` | Anchor outputs
66 | Msg("init", globalfeatures="", features=bitfield(13, 21)),
67 | # BOLT #9:
68 | # | 12/13 | `option_static_remotekey` | Static key for remote output
69 | Msg("init", globalfeatures="", features=bitfield(13)),
70 | # And not.
71 | Msg("init", globalfeatures="", features=""),
72 | ),
73 | ]
74 | run_runner(runner, merge_events_sequences(connections_events, test_events))
75 |
76 |
77 | def test_open_channel_from_accepter_side(runner: Runner) -> None:
78 | """Check the open channel from an accepter view point"""
79 | local_funding_privkey = "20"
80 | local_keyset = gen_random_keyset(int(local_funding_privkey))
81 | connections_events = connect_to_node_helper(
82 | runner=runner,
83 | tx_spendable=tx_spendable,
84 | conn_privkey="02",
85 | )
86 |
87 | # Accepter side: we initiate a new channel.
88 | test_events = [
89 | Msg(
90 | "open_channel",
91 | chain_hash=BitcoinUtils.blockchain_hash(),
92 | temporary_channel_id="00" * 32,
93 | funding_satoshis=funding_amount_for_utxo(0),
94 | push_msat=0,
95 | dust_limit_satoshis=546,
96 | max_htlc_value_in_flight_msat=4294967295,
97 | channel_reserve_satoshis=9998,
98 | htlc_minimum_msat=0,
99 | feerate_per_kw=253,
100 | # We use 5, because c-lightning runner uses 6, so this is different.
101 | to_self_delay=5,
102 | max_accepted_htlcs=483,
103 | funding_pubkey=pubkey_of(local_funding_privkey),
104 | revocation_basepoint=local_keyset.revocation_basepoint(),
105 | payment_basepoint=local_keyset.payment_basepoint(),
106 | delayed_payment_basepoint=local_keyset.delayed_payment_basepoint(),
107 | htlc_basepoint=local_keyset.htlc_basepoint(),
108 | first_per_commitment_point=local_keyset.per_commit_point(0),
109 | channel_flags=1,
110 | ),
111 | # Ignore unknown odd messages
112 | TryAll([], RawMsg(bytes.fromhex("270F"))),
113 | ExpectMsg(
114 | "accept_channel",
115 | temporary_channel_id=sent(),
116 | funding_pubkey=remote_funding_pubkey(),
117 | revocation_basepoint=remote_revocation_basepoint(),
118 | payment_basepoint=remote_payment_basepoint(),
119 | delayed_payment_basepoint=remote_delayed_payment_basepoint(),
120 | htlc_basepoint=remote_htlc_basepoint(),
121 | first_per_commitment_point=remote_per_commitment_point(0),
122 | minimum_depth=stash_field_from_event("accept_channel", dummy_val=3),
123 | channel_reserve_satoshis=9998,
124 | ),
125 | # Ignore unknown odd messages
126 | TryAll([], RawMsg(bytes.fromhex("270F"))),
127 | # Create and stash Funding object and FundingTx
128 | CreateFunding(
129 | *utxo(0),
130 | local_node_privkey="02",
131 | local_funding_privkey=local_funding_privkey,
132 | remote_node_privkey=runner.get_node_privkey(),
133 | remote_funding_privkey=remote_funding_privkey()
134 | ),
135 | Commit(
136 | funding=funding(),
137 | opener=Side.local,
138 | local_keyset=local_keyset,
139 | local_to_self_delay=rcvd("to_self_delay", int),
140 | remote_to_self_delay=sent("to_self_delay", int),
141 | local_amount=msat(sent("funding_satoshis", int)),
142 | remote_amount=0,
143 | local_dust_limit=546,
144 | remote_dust_limit=546,
145 | feerate=253,
146 | local_features=sent("init.features"),
147 | remote_features=rcvd("init.features"),
148 | ),
149 | Msg(
150 | "funding_created",
151 | temporary_channel_id=rcvd(),
152 | funding_txid=funding_txid(),
153 | funding_output_index=0,
154 | signature=commitsig_to_send(),
155 | ),
156 | ExpectMsg(
157 | "funding_signed",
158 | channel_id=channel_id(),
159 | signature=commitsig_to_recv(),
160 | ),
161 | # Mine it and get it deep enough to confirm channel.
162 | Block(
163 | blockheight=103,
164 | number=stash_field_from_event(
165 | "accept_channel", field_name="minimum_depth", dummy_val=3
166 | ),
167 | txs=[funding_tx()],
168 | ),
169 | ExpectMsg(
170 | "channel_ready",
171 | channel_id=channel_id(),
172 | second_per_commitment_point="032405cbd0f41225d5f203fe4adac8401321a9e05767c5f8af97d51d2e81fbb206",
173 | ),
174 | Msg(
175 | "channel_ready",
176 | channel_id=channel_id(),
177 | second_per_commitment_point="027eed8389cf8eb715d73111b73d94d2c2d04bf96dc43dfd5b0970d80b3617009d",
178 | ),
179 | # Ignore unknown odd messages
180 | TryAll([], RawMsg(bytes.fromhex("270F"))),
181 | ]
182 | run_runner(runner, merge_events_sequences(connections_events, test_events))
183 |
184 |
185 | def test_open_channel_opener_side(runner: Runner) -> None:
186 | local_funding_privkey = "20"
187 | local_keyset = gen_random_keyset(int(local_funding_privkey))
188 | connections_events = connect_to_node_helper(
189 | runner=runner,
190 | tx_spendable=tx_spendable,
191 | conn_privkey="02",
192 | )
193 |
194 | # Now we test the 'opener' side of an open_channel (node initiates)
195 | test_events = [
196 | FundChannel(amount=999877),
197 | # This gives a channel of 999877sat
198 | ExpectMsg(
199 | "open_channel",
200 | chain_hash=BitcoinUtils.blockchain_hash(),
201 | funding_satoshis=999877,
202 | push_msat=0,
203 | dust_limit_satoshis=stash_field_from_event("open_channel", dummy_val=546),
204 | htlc_minimum_msat=stash_field_from_event("open_channel", dummy_val=0),
205 | channel_reserve_satoshis=9998,
206 | to_self_delay=stash_field_from_event("open_channel", dummy_val=6),
207 | funding_pubkey=remote_funding_pubkey(),
208 | revocation_basepoint=remote_revocation_basepoint(),
209 | payment_basepoint=remote_payment_basepoint(),
210 | delayed_payment_basepoint=remote_delayed_payment_basepoint(),
211 | htlc_basepoint=remote_htlc_basepoint(),
212 | first_per_commitment_point=remote_per_commitment_point(0),
213 | # FIXME: Check more fields!
214 | channel_flags="01",
215 | ),
216 | Msg(
217 | "accept_channel",
218 | temporary_channel_id=rcvd(),
219 | dust_limit_satoshis=546,
220 | max_htlc_value_in_flight_msat=4294967295,
221 | channel_reserve_satoshis=9998,
222 | htlc_minimum_msat=0,
223 | minimum_depth=3,
224 | max_accepted_htlcs=483,
225 | # We use 5, because c-lightning runner uses 6, so this is different.
226 | to_self_delay=5,
227 | funding_pubkey=pubkey_of(local_funding_privkey),
228 | revocation_basepoint=local_keyset.revocation_basepoint(),
229 | payment_basepoint=local_keyset.payment_basepoint(),
230 | delayed_payment_basepoint=local_keyset.delayed_payment_basepoint(),
231 | htlc_basepoint=local_keyset.htlc_basepoint(),
232 | first_per_commitment_point=local_keyset.per_commit_point(0),
233 | ),
234 | # Ignore unknown odd messages
235 | TryAll([], RawMsg(bytes.fromhex("270F"))),
236 | ExpectMsg("funding_created", temporary_channel_id=rcvd("temporary_channel_id")),
237 | # Now we can finally stash the funding information.
238 | AcceptFunding(
239 | rcvd("funding_created.funding_txid"),
240 | funding_output_index=rcvd("funding_created.funding_output_index", int),
241 | funding_amount=rcvd("open_channel.funding_satoshis", int),
242 | local_node_privkey="02",
243 | local_funding_privkey=local_funding_privkey,
244 | remote_node_privkey=runner.get_node_privkey(),
245 | remote_funding_privkey=remote_funding_privkey(),
246 | ),
247 | Commit(
248 | funding=funding(),
249 | opener=Side.remote,
250 | local_keyset=local_keyset,
251 | local_to_self_delay=rcvd("open_channel.to_self_delay", int),
252 | remote_to_self_delay=sent("accept_channel.to_self_delay", int),
253 | local_amount=0,
254 | remote_amount=msat(rcvd("open_channel.funding_satoshis", int)),
255 | local_dust_limit=sent("accept_channel.dust_limit_satoshis", int),
256 | remote_dust_limit=rcvd("open_channel.dust_limit_satoshis", int),
257 | feerate=rcvd("open_channel.feerate_per_kw", int),
258 | local_features=sent("init.features"),
259 | remote_features=rcvd("init.features"),
260 | ),
261 | # Now we've created commit, we can check sig is valid!
262 | CheckEq(rcvd("funding_created.signature"), commitsig_to_recv()),
263 | Msg(
264 | "funding_signed",
265 | channel_id=channel_id(),
266 | signature=commitsig_to_send(),
267 | ),
268 | # It will broadcast tx
269 | ExpectTx(rcvd("funding_created.funding_txid")),
270 | # Mine three blocks to confirm channel.
271 | Block(blockheight=103, number=3),
272 | Msg(
273 | "channel_ready",
274 | channel_id=sent(),
275 | second_per_commitment_point=local_keyset.per_commit_point(1),
276 | ),
277 | ExpectMsg(
278 | "channel_ready",
279 | channel_id=sent(),
280 | second_per_commitment_point=remote_per_commitment_point(1),
281 | ),
282 | # Ignore unknown odd messages
283 | TryAll([], RawMsg(bytes.fromhex("270F"))),
284 | ]
285 | run_runner(runner, merge_events_sequences(connections_events, test_events))
286 |
287 |
288 | def test_open_channel_opener_side_wrong_announcement_signatures(runner: Runner) -> None:
289 | """Testing the case where the channel is announces in the correct way but one node
290 | send the wrong signature inside the `announcement_signatures` message."""
291 | from lnprototest.clightning import Runner as CLightningRunner
292 |
293 | connections_events = connect_to_node_helper(
294 | runner=runner,
295 | tx_spendable=tx_spendable,
296 | conn_privkey="02",
297 | )
298 | opts = {}
299 | open_channel_events = open_and_announce_channel_helper(runner, "02", opts=opts)
300 | pre_events = merge_events_sequences(connections_events, open_channel_events)
301 |
302 | dummy_sign = "138c93afb2013c39f959e70a163c3d6d8128cf72f8ae143f87b9d1fd6bb0ad30321116b9c58d69fca9fb33c214f681b664e53d5640abc2fdb972dc62a5571053"
303 | short_channel_id = opts["short_channel_id"]
304 |
305 | is_cln = isinstance(runner, CLightningRunner)
306 | test_events = [
307 | # BOLT 2:
308 | #
309 | # - Once both nodes have exchanged channel_ready (and optionally announcement_signatures),
310 | # the channel can be used to make payments via Hashed Time Locked Contracts.
311 | ExpectMsg(
312 | "announcement_signatures",
313 | channel_id=channel_id(),
314 | short_channel_id=short_channel_id,
315 | node_signature=stash_field_from_event(
316 | "announcement_signatures", dummy_val=dummy_sign
317 | ),
318 | bitcoin_signature=stash_field_from_event(
319 | "announcement_signatures", dummy_val=dummy_sign
320 | ),
321 | ignore=ExpectMsg.ignore_channel_update,
322 | ),
323 | # BOLT 7:
324 | # - if the node_signature OR the bitcoin_signature is NOT correct:
325 | # - MAY send a warning and close the connection, or send an error and fail the channel.
326 | #
327 | # In our case, we send an error and stop the open channel procedure. This approach is
328 | # considered overly strict since the peer can recover from it. However, this step is
329 | # optional. If the peer sends it, we assume that the signature must be correct.
330 | Msg(
331 | "announcement_signatures",
332 | channel_id=channel_id(),
333 | short_channel_id=short_channel_id,
334 | node_signature=stash_field_from_event(
335 | "announcement_signatures", dummy_val=dummy_sign
336 | ),
337 | bitcoin_signature=stash_field_from_event(
338 | "announcement_signatures", dummy_val=dummy_sign
339 | ),
340 | ),
341 | # FIXME: here there is an error but we are not able to catch
342 | # because core lightning is too fast in closing the connection.
343 | #
344 | # So we should change the OneOf to all exception and stop when
345 | # the first one succided
346 | Sequence(ExpectDisconnect(), enable=is_cln),
347 | Sequence(ExpectMsg("error"), enable=(not is_cln)),
348 | ]
349 | run_runner(runner, merge_events_sequences(pre_events, test_events))
350 |
--------------------------------------------------------------------------------
/tests/test_bolt2-02-reestablish.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | # Variations on adding an HTLC.
3 |
4 | from lnprototest import (
5 | TryAll,
6 | Sequence,
7 | Connect,
8 | Block,
9 | ExpectMsg,
10 | Msg,
11 | RawMsg,
12 | CreateFunding,
13 | Commit,
14 | Runner,
15 | Disconnect,
16 | remote_funding_pubkey,
17 | remote_revocation_basepoint,
18 | remote_payment_basepoint,
19 | remote_htlc_basepoint,
20 | remote_per_commitment_point,
21 | remote_delayed_payment_basepoint,
22 | Side,
23 | CheckEq,
24 | msat,
25 | remote_funding_privkey,
26 | negotiated,
27 | )
28 | from lnprototest.stash import (
29 | sent,
30 | rcvd,
31 | commitsig_to_send,
32 | commitsig_to_recv,
33 | channel_id,
34 | funding_txid,
35 | funding_tx,
36 | funding,
37 | stash_field_from_event,
38 | )
39 | from lnprototest.utils import (
40 | BitcoinUtils,
41 | run_runner,
42 | merge_events_sequences,
43 | utxo,
44 | tx_spendable,
45 | funding_amount_for_utxo,
46 | pubkey_of,
47 | gen_random_keyset,
48 | )
49 | from lnprototest.utils.ln_spec_utils import connect_to_node_helper
50 |
51 | # FIXME: bolt9.featurebits?
52 | # BOLT #9:
53 | # | 12/13 | `option_static_remotekey` | Static key for remote output
54 | static_remotekey = 13
55 |
56 | # BOLT #9:
57 | # | 0/1 | `option_data_loss_protect` | Requires or supports extra `channel_reestablish` fields
58 | data_loss_protect = 1
59 |
60 | # BOLT-a12da24dd0102c170365124782b46d9710950ac1 #9:
61 | # | 20/21 | `option_anchor_outputs` | Anchor outputs
62 | anchor_outputs = 21
63 |
64 |
65 | def test_reestablish(runner: Runner) -> None:
66 | local_funding_privkey = "20"
67 | local_keyset = gen_random_keyset(int(local_funding_privkey))
68 | connections_events = connect_to_node_helper(
69 | runner=runner,
70 | tx_spendable=tx_spendable,
71 | conn_privkey="02",
72 | )
73 |
74 | test_events = [
75 | Msg(
76 | "open_channel",
77 | chain_hash=BitcoinUtils.blockchain_hash(),
78 | temporary_channel_id="00" * 32,
79 | funding_satoshis=funding_amount_for_utxo(0),
80 | push_msat=0,
81 | dust_limit_satoshis=546,
82 | max_htlc_value_in_flight_msat=4294967295,
83 | channel_reserve_satoshis=9998,
84 | htlc_minimum_msat=0,
85 | feerate_per_kw=253,
86 | # clightning uses to_self_delay=6; we use 5 to test differentiation
87 | to_self_delay=5,
88 | max_accepted_htlcs=483,
89 | funding_pubkey=pubkey_of(local_funding_privkey),
90 | revocation_basepoint=local_keyset.revocation_basepoint(),
91 | payment_basepoint=local_keyset.payment_basepoint(),
92 | delayed_payment_basepoint=local_keyset.delayed_payment_basepoint(),
93 | htlc_basepoint=local_keyset.htlc_basepoint(),
94 | first_per_commitment_point=local_keyset.per_commit_point(0),
95 | channel_flags=1,
96 | ),
97 | ExpectMsg(
98 | "accept_channel",
99 | funding_pubkey=remote_funding_pubkey(),
100 | revocation_basepoint=remote_revocation_basepoint(),
101 | payment_basepoint=remote_payment_basepoint(),
102 | delayed_payment_basepoint=remote_delayed_payment_basepoint(),
103 | htlc_basepoint=remote_htlc_basepoint(),
104 | first_per_commitment_point=remote_per_commitment_point(0),
105 | minimum_depth=stash_field_from_event("accept_channel", dummy_val=3),
106 | channel_reserve_satoshis=9998,
107 | ),
108 | # Create and stash Funding object and FundingTx
109 | CreateFunding(
110 | *utxo(0),
111 | local_node_privkey="02",
112 | local_funding_privkey=local_funding_privkey,
113 | remote_node_privkey=runner.get_node_privkey(),
114 | remote_funding_privkey=remote_funding_privkey()
115 | ),
116 | Commit(
117 | funding=funding(),
118 | opener=Side.local,
119 | local_keyset=local_keyset,
120 | local_to_self_delay=rcvd("to_self_delay", int),
121 | remote_to_self_delay=sent("to_self_delay", int),
122 | local_amount=msat(sent("funding_satoshis", int)),
123 | remote_amount=0,
124 | local_dust_limit=546,
125 | remote_dust_limit=546,
126 | feerate=253,
127 | local_features=sent("init.features"),
128 | remote_features=rcvd("init.features"),
129 | ),
130 | Msg(
131 | "funding_created",
132 | temporary_channel_id=rcvd(),
133 | funding_txid=funding_txid(),
134 | funding_output_index=0,
135 | signature=commitsig_to_send(),
136 | ),
137 | ExpectMsg(
138 | "funding_signed", channel_id=channel_id(), signature=commitsig_to_recv()
139 | ),
140 | # Mine it and get it deep enough to confirm channel.
141 | Block(
142 | blockheight=103,
143 | number=stash_field_from_event(
144 | "accept_channel", field_name="minimum_depth", dummy_val=3
145 | ),
146 | txs=[funding_tx()],
147 | ),
148 | ExpectMsg(
149 | "channel_ready",
150 | channel_id=channel_id(),
151 | second_per_commitment_point=remote_per_commitment_point(1),
152 | ),
153 | Msg(
154 | "channel_ready",
155 | channel_id=channel_id(),
156 | second_per_commitment_point=local_keyset.per_commit_point(1),
157 | ),
158 | Disconnect(),
159 | Connect(connprivkey="02"),
160 | ExpectMsg("init"),
161 | # Reconnect with same features.
162 | Msg("init", globalfeatures="", features=sent("init.features")),
163 | # BOLT #2:
164 | # - if `next_revocation_number` equals 0:
165 | # - MUST set `your_last_per_commitment_secret` to all zeroes
166 | # - otherwise:
167 | # - MUST set `your_last_per_commitment_secret` to the last
168 | # `per_commitment_secret` it received
169 | ExpectMsg(
170 | "channel_reestablish",
171 | channel_id=channel_id(),
172 | next_commitment_number=1,
173 | next_revocation_number=0,
174 | your_last_per_commitment_secret="00" * 32,
175 | ),
176 | # BOLT #2:
177 | # The sending node:...
178 | # - if `option_static_remotekey` applies to the commitment
179 | # transaction:
180 | # - MUST set `my_current_per_commitment_point` to a valid point.
181 | # - otherwise:
182 | # - MUST set `my_current_per_commitment_point` to its commitment
183 | # point for the last signed commitment it received from its
184 | # channel peer (i.e. the commitment_point corresponding to the
185 | # commitment transaction the sender would use to unilaterally
186 | # close).
187 | Sequence(
188 | CheckEq(
189 | rcvd("my_current_per_commitment_point"), remote_per_commitment_point(0)
190 | ),
191 | enable=negotiated(
192 | sent("init.features"),
193 | rcvd("init.features"),
194 | excluded=[static_remotekey],
195 | ),
196 | ),
197 | # Ignore unknown odd messages
198 | TryAll([], RawMsg(bytes.fromhex("270F"))),
199 | Msg(
200 | "channel_reestablish",
201 | channel_id=channel_id(),
202 | next_commitment_number=1,
203 | next_revocation_number=0,
204 | your_last_per_commitment_secret="00" * 32,
205 | my_current_per_commitment_point=local_keyset.per_commit_point(0),
206 | ),
207 | # FIXME: Check that they error and unilateral close if we give
208 | # the wrong info!
209 | ]
210 |
211 | run_runner(runner, merge_events_sequences(connections_events, test_events))
212 |
--------------------------------------------------------------------------------
/tests/test_bolt2-30-channel_type-open-accept-tlvs.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 |
3 | from lnprototest import (
4 | TryAll,
5 | Connect,
6 | Block,
7 | ExpectMsg,
8 | Msg,
9 | Runner,
10 | KeySet,
11 | bitfield,
12 | channel_type_csv,
13 | ExpectError,
14 | )
15 | from lnprototest.utils import (
16 | BitcoinUtils,
17 | tx_spendable,
18 | funding_amount_for_utxo,
19 | pubkey_of,
20 | )
21 | from typing import Any
22 | import pytest
23 |
24 |
25 | def test_open_channel(runner: Runner, with_proposal: Any) -> None:
26 | """Tests for https://github.com/lightningnetwork/lightning-rfc/pull/880"""
27 | with_proposal(channel_type_csv)
28 |
29 | # This is not a feature bit, so use support_ to mark it.
30 | if runner.has_option("supports_open_accept_channel_type") is None:
31 | pytest.skip("Needs supports_open_accept_channel_type")
32 |
33 | local_funding_privkey = "20"
34 |
35 | local_keyset = KeySet(
36 | revocation_base_secret="21",
37 | payment_base_secret="22",
38 | htlc_base_secret="24",
39 | delayed_payment_base_secret="23",
40 | shachain_seed="00" * 32,
41 | )
42 |
43 | test = [
44 | Block(blockheight=102, txs=[tx_spendable]),
45 | Connect(connprivkey="02"),
46 | ExpectMsg("init"),
47 | Msg(
48 | "init",
49 | globalfeatures=runner.runner_features(globals=True),
50 | features=runner.runner_features(additional_features=[13]),
51 | ),
52 | Msg(
53 | "open_channel",
54 | chain_hash=BitcoinUtils.blockchain_hash(),
55 | temporary_channel_id="00" * 32,
56 | funding_satoshis=funding_amount_for_utxo(0),
57 | push_msat=0,
58 | dust_limit_satoshis=546,
59 | max_htlc_value_in_flight_msat=4294967295,
60 | channel_reserve_satoshis=9998,
61 | htlc_minimum_msat=0,
62 | feerate_per_kw=253,
63 | # We use 5, because core-lightning runner uses 6, so this is different.
64 | to_self_delay=5,
65 | max_accepted_htlcs=483,
66 | funding_pubkey=pubkey_of(local_funding_privkey),
67 | revocation_basepoint=local_keyset.revocation_basepoint(),
68 | payment_basepoint=local_keyset.payment_basepoint(),
69 | delayed_payment_basepoint=local_keyset.delayed_payment_basepoint(),
70 | htlc_basepoint=local_keyset.htlc_basepoint(),
71 | first_per_commitment_point=local_keyset.per_commit_point(0),
72 | channel_flags=1,
73 | # We negotiate *down* to a simple static channel
74 | tlvs="{channel_type={type=" + bitfield(12) + "}}",
75 | ),
76 | # BOLT #2
77 | # - if it sets `channel_type`:
78 | # - MUST set it to the `channel_type` from `open_channel`
79 | ExpectMsg("accept_channel", tlvs="{channel_type={type=" + bitfield(12) + "}}"),
80 | ]
81 |
82 | runner.run(test)
83 |
84 |
85 | def test_open_channel_bad_type(runner: Runner, with_proposal: Any) -> None:
86 | """Tests for https://github.com/lightningnetwork/lightning-rfc/pull/880"""
87 | with_proposal(channel_type_csv)
88 |
89 | # This is not a feature bit, so use support_ to mark it.
90 | if runner.has_option("supports_open_accept_channel_type") is None:
91 | pytest.skip("Needs supports_open_accept_channel_type")
92 |
93 | local_funding_privkey = "20"
94 |
95 | local_keyset = KeySet(
96 | revocation_base_secret="21",
97 | payment_base_secret="22",
98 | htlc_base_secret="24",
99 | delayed_payment_base_secret="23",
100 | shachain_seed="00" * 32,
101 | )
102 |
103 | test = [
104 | Block(blockheight=102, txs=[tx_spendable]),
105 | Connect(connprivkey="02"),
106 | ExpectMsg("init"),
107 | # BOLT #9:
108 | # | 12/13 | `option_static_remotekey` | Static key for remote output
109 | Msg(
110 | "init",
111 | globalfeatures=runner.runner_features(globals=True),
112 | features=runner.runner_features(additional_features=[12]),
113 | ),
114 | Msg(
115 | "open_channel",
116 | chain_hash=BitcoinUtils.blockchain_hash(),
117 | temporary_channel_id="00" * 32,
118 | funding_satoshis=funding_amount_for_utxo(0),
119 | push_msat=0,
120 | dust_limit_satoshis=546,
121 | max_htlc_value_in_flight_msat=4294967295,
122 | channel_reserve_satoshis=9998,
123 | htlc_minimum_msat=0,
124 | feerate_per_kw=253,
125 | # We use 5, because core-lightning runner uses 6, so this is different.
126 | to_self_delay=5,
127 | max_accepted_htlcs=483,
128 | funding_pubkey=pubkey_of(local_funding_privkey),
129 | revocation_basepoint=local_keyset.revocation_basepoint(),
130 | payment_basepoint=local_keyset.payment_basepoint(),
131 | delayed_payment_basepoint=local_keyset.delayed_payment_basepoint(),
132 | htlc_basepoint=local_keyset.htlc_basepoint(),
133 | first_per_commitment_point=local_keyset.per_commit_point(0),
134 | channel_flags=1,
135 | tlvs="{channel_type={type=" + bitfield(100) + "}}",
136 | ),
137 | # BOLT #2
138 | # The receiving node MUST fail the channel if:
139 | # - It supports `channel_types` and none of the `channel_types`
140 | # are suitable.
141 | ExpectError(),
142 | ]
143 |
144 | runner.run(test)
145 |
--------------------------------------------------------------------------------
/tests/test_bolt7-01-channel_announcement-success.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | # Simple gossip tests.
3 |
4 | from lnprototest import (
5 | Connect,
6 | Block,
7 | ExpectMsg,
8 | Msg,
9 | RawMsg,
10 | Funding,
11 | Side,
12 | MustNotMsg,
13 | Disconnect,
14 | Runner,
15 | )
16 | from lnprototest.utils import tx_spendable, utxo
17 | import time
18 |
19 |
20 | def test_gossip_forget_channel_after_12_blocks(runner: Runner) -> None:
21 | # Make up a channel between nodes 02 and 03, using bitcoin privkeys 10 and 20
22 | funding, funding_tx = Funding.from_utxo(
23 | *utxo(0),
24 | local_node_privkey="02",
25 | local_funding_privkey="10",
26 | remote_node_privkey="03",
27 | remote_funding_privkey="20"
28 | )
29 |
30 | test = [
31 | Block(blockheight=102, txs=[tx_spendable]),
32 | Connect(connprivkey="03"),
33 | ExpectMsg("init"),
34 | Msg(
35 | "init",
36 | globalfeatures=runner.runner_features(globals=True),
37 | features=runner.runner_features(),
38 | ),
39 | Block(blockheight=103, number=6, txs=[funding_tx]),
40 | RawMsg(funding.channel_announcement("103x1x0", "")),
41 | # New peer connects, asking for initial_routing_sync. We *won't* relay channel_announcement, as there is no
42 | # channel_update.
43 | Connect(connprivkey="05"),
44 | ExpectMsg("init"),
45 | Msg(
46 | "init",
47 | globalfeatures=runner.runner_features(globals=True),
48 | features=runner.runner_features(additional_features=[3]),
49 | ),
50 | MustNotMsg("channel_announcement"),
51 | Disconnect(),
52 | RawMsg(
53 | funding.channel_update(
54 | "103x1x0",
55 | Side.local,
56 | disable=False,
57 | cltv_expiry_delta=144,
58 | htlc_minimum_msat=0,
59 | fee_base_msat=1000,
60 | fee_proportional_millionths=10,
61 | timestamp=int(time.time()),
62 | htlc_maximum_msat=2000000,
63 | ),
64 | connprivkey="03",
65 | ),
66 | # Now we'll relay to a new peer.
67 | Connect(connprivkey="05"),
68 | ExpectMsg("init"),
69 | Msg(
70 | "init",
71 | globalfeatures=runner.runner_features(globals=True),
72 | features=runner.runner_features(additional_features=[3]),
73 | ),
74 | ExpectMsg("channel_announcement", short_channel_id="103x1x0"),
75 | ExpectMsg(
76 | "channel_update",
77 | short_channel_id="103x1x0",
78 | message_flags=1,
79 | channel_flags=0,
80 | ),
81 | Disconnect(),
82 | # BOLT #7:
83 | # - once its funding output has been spent OR reorganized out:
84 | # - SHOULD forget a channel after a 12-block delay.
85 | Block(blockheight=109, number=13, txs=[funding.close_tx(200, "99")]),
86 | Connect(connprivkey="05"),
87 | ExpectMsg("init"),
88 | Msg(
89 | "init",
90 | globalfeatures=runner.runner_features(globals=True),
91 | features=runner.runner_features(additional_features=[3]),
92 | ),
93 | MustNotMsg("channel_announcement"),
94 | MustNotMsg("channel_update"),
95 | ]
96 |
97 | runner.run(test)
98 |
--------------------------------------------------------------------------------
/tests/test_bolt7-02-channel_announcement-failure.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | # Tests for malformed/bad channel_announcement
3 |
4 | from lnprototest import (
5 | Connect,
6 | Block,
7 | ExpectMsg,
8 | Msg,
9 | RawMsg,
10 | ExpectError,
11 | Funding,
12 | Side,
13 | MustNotMsg,
14 | Runner,
15 | TryAll,
16 | Sig,
17 | )
18 | import time
19 | from typing import cast
20 | from lnprototest.utils import utxo, tx_spendable
21 |
22 |
23 | # FIXME: Make this work in-place!
24 | def corrupt_sig(sig: Sig) -> Sig:
25 | hashval = bytearray(cast(bytes, sig.hashval))
26 | hashval[-1] ^= 1
27 | return Sig(sig.privkey.secret.hex(), hashval.hex())
28 |
29 |
30 | def test_premature_channel_announcement(runner: Runner) -> None:
31 | # It's allowed (even encouraged!) to cache premature
32 | # channel_announcements, so we separate this from the other tests.
33 |
34 | funding, funding_tx = Funding.from_utxo(
35 | *utxo(0),
36 | local_node_privkey="02",
37 | local_funding_privkey="10",
38 | remote_node_privkey="03",
39 | remote_funding_privkey="20"
40 | )
41 |
42 | test = [
43 | Block(blockheight=102, txs=[tx_spendable]),
44 | Connect(connprivkey="03"),
45 | ExpectMsg("init"),
46 | Msg(
47 | "init",
48 | globalfeatures=runner.runner_features(globals=True),
49 | features=runner.runner_features(),
50 | ),
51 | # txid 189c40b0728f382fe91c87270926584e48e0af3a6789f37454afee6c7560311d
52 | Block(blockheight=103, txs=[funding_tx]),
53 | TryAll(
54 | # Invalid `channel_announcement`: short_channel_id too young.
55 | [RawMsg(funding.channel_announcement("103x1x0", ""))],
56 | # Invalid `channel_announcement`: short_channel_id *still* too young.
57 | [
58 | Block(blockheight=104, number=4),
59 | RawMsg(funding.channel_announcement("103x1x0", "")),
60 | ],
61 | ),
62 | # Needs a channel_update if it were to relay.
63 | RawMsg(
64 | funding.channel_update(
65 | "103x1x0",
66 | Side.local,
67 | disable=False,
68 | cltv_expiry_delta=144,
69 | htlc_minimum_msat=0,
70 | fee_base_msat=1000,
71 | fee_proportional_millionths=10,
72 | timestamp=int(time.time()),
73 | htlc_maximum_msat=2000000,
74 | )
75 | ),
76 | # New peer connects, asking for initial_routing_sync. We *won't* relay channel_announcement.
77 | Connect(connprivkey="05"),
78 | ExpectMsg("init"),
79 | Msg(
80 | "init",
81 | globalfeatures=runner.runner_features(globals=True),
82 | features=runner.runner_features(additional_features=[3]),
83 | ),
84 | MustNotMsg("channel_announcement"),
85 | MustNotMsg("channel_update"),
86 | ]
87 |
88 | runner.run(test)
89 |
90 |
91 | def test_bad_announcement(runner: Runner) -> None:
92 | funding, funding_tx = Funding.from_utxo(
93 | *utxo(0),
94 | local_node_privkey="02",
95 | local_funding_privkey="10",
96 | remote_node_privkey="03",
97 | remote_funding_privkey="20"
98 | )
99 |
100 | # ### Ignored:
101 | ann_bad_chainhash = funding.channel_announcement("103x1x0", "")
102 | ann_bad_chainhash.fields["chain_hash"] = bytes.fromhex(
103 | "6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000"
104 | )
105 |
106 | ann_bad_scid_dne = funding.channel_announcement("103x2x0", "")
107 |
108 | ann_bad_scid_out_dne = funding.channel_announcement("103x1x1", "")
109 |
110 | ann_bad_bitcoin_key1 = Funding(
111 | funding.txid,
112 | funding.output_index,
113 | funding.amount,
114 | local_node_privkey="02",
115 | local_funding_privkey="10",
116 | remote_node_privkey="03",
117 | remote_funding_privkey="21",
118 | ).channel_announcement("103x1x0", "")
119 |
120 | ann_bad_bitcoin_key2 = Funding(
121 | funding.txid,
122 | funding.output_index,
123 | funding.amount,
124 | local_node_privkey="02",
125 | local_funding_privkey="11",
126 | remote_node_privkey="03",
127 | remote_funding_privkey="20",
128 | ).channel_announcement("103x1x0", "")
129 |
130 | # ### These should cause an error
131 | ann_bad_nodesig1 = funding.channel_announcement("103x1x0", "")
132 | ann_bad_nodesig1.fields["node_signature_1"] = corrupt_sig(
133 | ann_bad_nodesig1.fields["node_signature_1"]
134 | )
135 |
136 | ann_bad_nodesig2 = funding.channel_announcement("103x1x0", "")
137 | ann_bad_nodesig2.fields["node_signature_2"] = corrupt_sig(
138 | ann_bad_nodesig2.fields["node_signature_2"]
139 | )
140 |
141 | ann_bad_bitcoinsig1 = funding.channel_announcement("103x1x0", "")
142 | ann_bad_bitcoinsig1.fields["bitcoin_signature_1"] = corrupt_sig(
143 | ann_bad_bitcoinsig1.fields["bitcoin_signature_1"]
144 | )
145 |
146 | ann_bad_bitcoinsig2 = funding.channel_announcement("103x1x0", "")
147 | ann_bad_bitcoinsig2.fields["bitcoin_signature_2"] = corrupt_sig(
148 | ann_bad_bitcoinsig2.fields["bitcoin_signature_2"]
149 | )
150 |
151 | test = [
152 | Block(blockheight=102, txs=[tx_spendable]),
153 | Connect(connprivkey="03"),
154 | ExpectMsg("init"),
155 | Msg(
156 | "init",
157 | globalfeatures=runner.runner_features(globals=True),
158 | features=runner.runner_features(),
159 | ),
160 | # txid 189c40b0728f382fe91c87270926584e48e0af3a6789f37454afee6c7560311d
161 | Block(blockheight=103, number=6, txs=[funding_tx]),
162 | TryAll(
163 | # These are all ignored
164 | # BOLT #7:
165 | # - if the specified `chain_hash` is unknown to the receiver:
166 | # - MUST ignore the message.
167 | [
168 | TryAll(
169 | [RawMsg(ann_bad_chainhash)],
170 | # BOLT #7:
171 | # - if the `short_channel_id`'s output does NOT correspond to a P2WSH (using
172 | # `bitcoin_key_1` and `bitcoin_key_2`, as specified in
173 | # [BOLT #3](03-transactions.md#funding-transaction-output)) OR the output is
174 | # spent:
175 | # - MUST ignore the message.
176 | [
177 | RawMsg(ann_bad_scid_dne),
178 | # Needs a channel_update if it were to relay.
179 | RawMsg(
180 | funding.channel_update(
181 | "103x2x0",
182 | Side.local,
183 | disable=False,
184 | cltv_expiry_delta=144,
185 | htlc_minimum_msat=0,
186 | fee_base_msat=1000,
187 | fee_proportional_millionths=10,
188 | timestamp=int(time.time()),
189 | htlc_maximum_msat=2000000,
190 | )
191 | ),
192 | ],
193 | [
194 | RawMsg(ann_bad_scid_out_dne),
195 | # Needs a channel_update if it were to relay.
196 | RawMsg(
197 | funding.channel_update(
198 | "103x1x1",
199 | Side.local,
200 | disable=False,
201 | cltv_expiry_delta=144,
202 | htlc_minimum_msat=0,
203 | fee_base_msat=1000,
204 | fee_proportional_millionths=10,
205 | timestamp=int(time.time()),
206 | htlc_maximum_msat=2000000,
207 | )
208 | ),
209 | ],
210 | [RawMsg(ann_bad_bitcoin_key1)],
211 | [RawMsg(ann_bad_bitcoin_key2)],
212 | ),
213 | # Needs a channel_update if it were to relay.
214 | RawMsg(
215 | funding.channel_update(
216 | "103x1x0",
217 | Side.local,
218 | disable=False,
219 | cltv_expiry_delta=144,
220 | htlc_minimum_msat=0,
221 | fee_base_msat=1000,
222 | fee_proportional_millionths=10,
223 | timestamp=int(time.time()),
224 | htlc_maximum_msat=2000000,
225 | )
226 | ),
227 | # New peer connects, asking for initial_routing_sync. We *won't* relay channel_announcement.
228 | Connect(connprivkey="05"),
229 | ExpectMsg("init"),
230 | Msg(
231 | "init",
232 | globalfeatures=runner.runner_features(globals=True),
233 | features=runner.runner_features(additional_features=[3]),
234 | ),
235 | MustNotMsg("channel_announcement"),
236 | MustNotMsg("channel_update"),
237 | ],
238 | # BOLT #7:
239 | # - if the specified chain_hash is unknown to the receiver:
240 | # - MUST ignore the message.
241 | # - otherwise:
242 | # - if bitcoin_signature_1, bitcoin_signature_2, node_signature_1 OR node_signature_2 are invalid OR NOT correct:
243 | # - SHOULD send a warning.
244 | # - MAY close the connection.
245 | # - MUST ignore the message.
246 | [
247 | TryAll(
248 | [RawMsg(ann_bad_nodesig1)],
249 | [RawMsg(ann_bad_nodesig2)],
250 | [RawMsg(ann_bad_bitcoinsig1)],
251 | [RawMsg(ann_bad_bitcoinsig2)],
252 | ),
253 | ExpectMsg("warning"),
254 | ],
255 | ),
256 | ]
257 |
258 | runner.run(test)
259 |
--------------------------------------------------------------------------------
/tests/test_bolt7-10-gossip-filter.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | # Tests for gossip_timestamp_filter
3 | from lnprototest import (
4 | Connect,
5 | Block,
6 | ExpectMsg,
7 | Msg,
8 | RawMsg,
9 | Side,
10 | MustNotMsg,
11 | Disconnect,
12 | AnyOrder,
13 | Runner,
14 | Funding,
15 | bitfield,
16 | )
17 | import pytest
18 | import time
19 | from lnprototest.utils import utxo, tx_spendable
20 |
21 |
22 | def test_gossip_timestamp_filter(runner: Runner) -> None:
23 | if runner.has_option("option_gossip_queries") is None:
24 | pytest.skip("Needs option_gossip_queries")
25 |
26 | funding1, funding1_tx = Funding.from_utxo(
27 | *utxo(0),
28 | local_node_privkey="02",
29 | local_funding_privkey="10",
30 | remote_node_privkey="03",
31 | remote_funding_privkey="20"
32 | )
33 |
34 | funding2, funding2_tx = Funding.from_utxo(
35 | *utxo(1),
36 | local_node_privkey="04",
37 | local_funding_privkey="30",
38 | remote_node_privkey="05",
39 | remote_funding_privkey="40"
40 | )
41 |
42 | timestamp1 = int(time.time())
43 | timestamp2 = timestamp1 + 1
44 |
45 | test = [
46 | Block(blockheight=102, txs=[tx_spendable]),
47 | Connect(connprivkey="03"),
48 | ExpectMsg("init"),
49 | Msg(
50 | "init",
51 | globalfeatures=runner.runner_features(globals=True),
52 | features=runner.runner_features(),
53 | ),
54 | # txid 189c40b0728f382fe91c87270926584e48e0af3a6789f37454afee6c7560311d
55 | Block(blockheight=103, number=6, txs=[funding1_tx]),
56 | RawMsg(funding1.channel_announcement("103x1x0", "")),
57 | RawMsg(
58 | funding1.node_announcement(
59 | Side.local, "", (1, 2, 3), "foobar", b"", timestamp1
60 | )
61 | ),
62 | # New peer connects, asks for gossip_timestamp_filter=all. We *won't* relay channel_announcement,
63 | # as there is no channel_update.
64 | Connect(connprivkey="05"),
65 | ExpectMsg("init"),
66 | # BOLT #9:
67 | # | 6/7 | `gossip_queries` | More sophisticated gossip control
68 | Msg(
69 | "init",
70 | globalfeatures=runner.runner_features(globals=True),
71 | features=runner.runner_features(additional_features=[6]),
72 | ),
73 | Msg(
74 | "gossip_timestamp_filter",
75 | chain_hash="06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f",
76 | first_timestamp=0,
77 | timestamp_range=4294967295,
78 | ),
79 | MustNotMsg("channel_announcement"),
80 | MustNotMsg("channel_update"),
81 | MustNotMsg("node_announcement"),
82 | Disconnect(),
83 | # Now, with channel update
84 | RawMsg(
85 | funding1.channel_update(
86 | side=Side.local,
87 | short_channel_id="103x1x0",
88 | disable=False,
89 | cltv_expiry_delta=144,
90 | htlc_minimum_msat=0,
91 | fee_base_msat=1000,
92 | fee_proportional_millionths=10,
93 | timestamp=timestamp1,
94 | htlc_maximum_msat=2000000,
95 | ),
96 | connprivkey="03",
97 | ),
98 | # New peer connects, asks for gossip_timestamp_filter=all. update and node announcement will be relayed.
99 | Connect(connprivkey="05"),
100 | ExpectMsg("init"),
101 | Msg(
102 | "init",
103 | globalfeatures=runner.runner_features(globals=True),
104 | features=runner.runner_features(additional_features=[6]),
105 | ),
106 | Msg(
107 | "gossip_timestamp_filter",
108 | chain_hash="06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f",
109 | first_timestamp=0,
110 | timestamp_range=4294967295,
111 | ),
112 | ExpectMsg("channel_announcement", short_channel_id="103x1x0"),
113 | AnyOrder(
114 | ExpectMsg("channel_update", short_channel_id="103x1x0"),
115 | ExpectMsg("node_announcement"),
116 | ),
117 | Disconnect(),
118 | # BOLT #7:
119 | # The receiver:
120 | # - SHOULD send all gossip messages whose `timestamp` is greater or
121 | # equal to `first_timestamp`, and less than `first_timestamp` plus
122 | # `timestamp_range`.
123 | Connect(connprivkey="05"),
124 | ExpectMsg("init"),
125 | Msg(
126 | "init",
127 | globalfeatures=runner.runner_features(globals=True),
128 | features=runner.runner_features(additional_features=[6]),
129 | ),
130 | Msg(
131 | "gossip_timestamp_filter",
132 | chain_hash="06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f",
133 | first_timestamp=1000,
134 | timestamp_range=timestamp1 - 1000,
135 | ),
136 | MustNotMsg("channel_announcement"),
137 | MustNotMsg("channel_update"),
138 | MustNotMsg("node_announcement"),
139 | Disconnect(),
140 | Connect(connprivkey="05"),
141 | ExpectMsg("init"),
142 | Msg(
143 | "init",
144 | globalfeatures=runner.runner_features(globals=True),
145 | features=runner.runner_features(additional_features=[6]),
146 | ),
147 | Msg(
148 | "gossip_timestamp_filter",
149 | chain_hash="06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f",
150 | first_timestamp=timestamp1 + 1,
151 | timestamp_range=4294967295,
152 | ),
153 | MustNotMsg("channel_announcement"),
154 | MustNotMsg("channel_update"),
155 | MustNotMsg("node_announcement"),
156 | Disconnect(),
157 | # These two succeed in getting the gossip, then stay connected for next test.
158 | Connect(connprivkey="05"),
159 | ExpectMsg("init"),
160 | Msg(
161 | "init",
162 | globalfeatures=runner.runner_features(globals=True),
163 | features=runner.runner_features(additional_features=[6]),
164 | ),
165 | Msg(
166 | "gossip_timestamp_filter",
167 | chain_hash="06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f",
168 | first_timestamp=timestamp1,
169 | timestamp_range=4294967295,
170 | ),
171 | ExpectMsg("channel_announcement", short_channel_id="103x1x0"),
172 | AnyOrder(
173 | ExpectMsg("channel_update", short_channel_id="103x1x0"),
174 | ExpectMsg("node_announcement"),
175 | ),
176 | Connect(connprivkey="06"),
177 | ExpectMsg("init"),
178 | Msg(
179 | "init",
180 | globalfeatures=runner.runner_features(globals=True),
181 | features=runner.runner_features(additional_features=[6]),
182 | ),
183 | Msg(
184 | "gossip_timestamp_filter",
185 | chain_hash="06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f",
186 | first_timestamp=1000,
187 | timestamp_range=timestamp1 - 1000 + 1,
188 | ),
189 | ExpectMsg("channel_announcement", short_channel_id="103x1x0"),
190 | AnyOrder(
191 | ExpectMsg("channel_update", short_channel_id="103x1x0"),
192 | ExpectMsg("node_announcement"),
193 | ),
194 | # BOLT #7:
195 | # - SHOULD restrict future gossip messages to those whose `timestamp`
196 | # is greater or equal to `first_timestamp`, and less than
197 | # `first_timestamp` plus `timestamp_range`.
198 | Block(blockheight=109, number=6, txs=[funding2_tx]),
199 | RawMsg(funding2.channel_announcement("109x1x0", ""), connprivkey="03"),
200 | RawMsg(
201 | funding2.channel_update(
202 | side=Side.local,
203 | short_channel_id="109x1x0",
204 | disable=False,
205 | cltv_expiry_delta=144,
206 | htlc_minimum_msat=0,
207 | fee_base_msat=1000,
208 | fee_proportional_millionths=10,
209 | timestamp=timestamp2,
210 | htlc_maximum_msat=2000000,
211 | )
212 | ),
213 | RawMsg(
214 | funding2.channel_update(
215 | side=Side.remote,
216 | short_channel_id="109x1x0",
217 | disable=False,
218 | cltv_expiry_delta=144,
219 | htlc_minimum_msat=0,
220 | fee_base_msat=1000,
221 | fee_proportional_millionths=10,
222 | timestamp=timestamp2,
223 | htlc_maximum_msat=2000000,
224 | )
225 | ),
226 | RawMsg(
227 | funding2.node_announcement(
228 | Side.local, "", (1, 2, 3), "foobar2", b"", timestamp2
229 | )
230 | ),
231 | # 005's filter covers this, 006's doesn't.
232 | ExpectMsg("channel_announcement", short_channel_id="109x1x0", connprivkey="05"),
233 | AnyOrder(
234 | ExpectMsg("channel_update", short_channel_id="109x1x0", channel_flags=0),
235 | ExpectMsg("channel_update", short_channel_id="109x1x0", channel_flags=1),
236 | ExpectMsg("node_announcement"),
237 | ),
238 | MustNotMsg("channel_announcement", connprivkey="06"),
239 | MustNotMsg("channel_update"),
240 | MustNotMsg("node_announcement"),
241 | ]
242 |
243 | runner.run(test)
244 |
--------------------------------------------------------------------------------
/tests/test_bolt7-20-query_channel_range.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | # Tests for gossip_timestamp_filter
3 | from lnprototest import (
4 | Connect,
5 | Block,
6 | ExpectMsg,
7 | Msg,
8 | RawMsg,
9 | Funding,
10 | Event,
11 | Side,
12 | MustNotMsg,
13 | OneOf,
14 | Runner,
15 | bitfield,
16 | TryAll,
17 | Sequence,
18 | CheckEq,
19 | EventError,
20 | namespace,
21 | )
22 | from lnprototest.utils import BitcoinUtils, tx_spendable, utxo
23 | from typing import Optional
24 | import pytest
25 | import time
26 | import io
27 | import zlib
28 | import crc32c
29 | from pyln.spec.bolt7 import channel_update_timestamps
30 | from pyln.proto.message import Message
31 |
32 | # Note for gossip_channel_range: we are *allowed* to return a superset
33 | # of what they ask, so if someone does that this test must be modified
34 | # to accept it (add an option_IMPL_gossip_query_superset?).
35 | #
36 | # Meanwhile, we assume an exact reply.
37 |
38 |
39 | def encode_timestamps(t1: int = 0, t2: int = 0) -> str:
40 | # BOLT #7:
41 | # For a single `channel_update`, timestamps are encoded as:
42 | #
43 | # 1. subtype: `channel_update_timestamps`
44 | # 2. data:
45 | # * [`u32`:`timestamp_node_id_1`]
46 | # * [`u32`:`timestamp_node_id_2`]
47 | #
48 | # Where:
49 | # * `timestamp_node_id_1` is the timestamp of the `channel_update` for
50 | # `node_id_1`, or 0 if there was no `channel_update` from that node.
51 | # * `timestamp_node_id_2` is the timestamp of the `channel_update` for
52 | # `node_id_2`, or 0 if there was no `channel_update` from that node.
53 | v, _ = channel_update_timestamps.val_from_str(
54 | "{{timestamp_node_id_1={},timestamp_node_id_2={}}}".format(t1, t2)
55 | )
56 |
57 | buf = io.BytesIO()
58 | channel_update_timestamps.write(buf, v, {})
59 | return buf.getvalue().hex()
60 |
61 |
62 | def decode_timestamps(runner: "Runner", event: Event, field: str) -> str:
63 | # Get timestamps from last reply_channel_range msg
64 | timestamps = runner.get_stash(event, "ExpectMsg")[-1][1]["tlvs"]["timestamps_tlv"]
65 |
66 | # BOLT #7:
67 | # Encoding types:
68 | # * `0`: uncompressed array of `short_channel_id` types, in ascending
69 | # order.
70 | # * `1`: array of `short_channel_id` types, in ascending order, compressed
71 | # with zlib deflate[1](#reference-1)
72 | if timestamps["encoding_type"] == 0:
73 | b = bytes.fromhex(timestamps["encoded_timestamps"])
74 | elif timestamps["encoding_type"] == 1:
75 | b = zlib.decompress(bytes.fromhex(timestamps["encoded_timestamps"]))
76 | else:
77 | raise EventError(event, "Unknown encoding type: {}".format(timestamps))
78 |
79 | return b.hex()
80 |
81 |
82 | def decode_scids(runner: "Runner", event: Event, field: str) -> str:
83 | # Nothing to decode if dummy runner.
84 | if runner._is_dummy():
85 | return ""
86 |
87 | # Get encoded_short_ids from last msg.
88 | encoded = bytes.fromhex(
89 | runner.get_stash(event, "ExpectMsg")[-1][1]["encoded_short_ids"]
90 | )
91 | # BOLT #7:
92 | # Encoding types:
93 | # * `0`: uncompressed array of `short_channel_id` types, in ascending
94 | # order.
95 | # * `1`: array of `short_channel_id` types, in ascending order, compressed
96 | # with zlib deflate[1](#reference-1)
97 | if encoded[0] == 0:
98 | b = encoded[1:]
99 | elif encoded[0] == 1:
100 | b = zlib.decompress(encoded[1:])
101 | else:
102 | raise EventError(
103 | event, "Unknown encoding type {}: {}".format(encoded[0], encoded.hex())
104 | )
105 |
106 | scidtype = namespace().get_fundamentaltype("short_channel_id")
107 | arr = []
108 | buf = io.BytesIO(b)
109 | while True:
110 | scid = scidtype.read(buf, {})
111 | if scid is None:
112 | break
113 | arr.append(scid)
114 |
115 | return ",".join([scidtype.val_to_str(a, {}) for a in arr])
116 |
117 |
118 | def calc_checksum(update: Message) -> int:
119 | # BOLT #7: The checksum of a `channel_update` is the CRC32C checksum as
120 | # specified in [RFC3720](https://tools.ietf.org/html/rfc3720#appendix-B.4)
121 | # of this `channel_update` without its `signature` and `timestamp` fields.
122 | bufio = io.BytesIO()
123 | update.write(bufio)
124 | buf = bufio.getvalue()
125 |
126 | # BOLT #7:
127 | # 1. type: 258 (`channel_update`)
128 | # 2. data:
129 | # * [`signature`:`signature`]
130 | # * [`chain_hash`:`chain_hash`]
131 | # * [`short_channel_id`:`short_channel_id`]
132 | # * [`u32`:`timestamp`]
133 | # * [`byte`:`message_flags`]
134 |
135 | # Note: 2 bytes for `type` field
136 | return crc32c.crc32c(buf[2 + 64 : 2 + 64 + 32 + 8] + buf[2 + 64 + 32 + 8 + 4 :])
137 |
138 |
139 | def update_checksums(update1: Optional[Message], update2: Optional[Message]) -> str:
140 | # BOLT #7:
141 | # For a single `channel_update`, checksums are encoded as:
142 | #
143 | # 1. subtype: `channel_update_checksums`
144 | # 2. data:
145 | # * [`u32`:`checksum_node_id_1`]
146 | # * [`u32`:`checksum_node_id_2`]
147 | #
148 | # Where:
149 | # * `checksum_node_id_1` is the checksum of the `channel_update` for
150 | # `node_id_1`, or 0 if there was no `channel_update` from that node.
151 | # * `checksum_node_id_2` is the checksum of the `channel_update` for
152 | # `node_id_2`, or 0 if there was no `channel_update` from that node.
153 | if update1:
154 | csum1 = calc_checksum(update1)
155 | else:
156 | csum1 = 0
157 |
158 | if update2:
159 | csum2 = calc_checksum(update2)
160 | else:
161 | csum2 = 0
162 |
163 | return "{{checksum_node_id_1={},checksum_node_id_2={}}}".format(csum1, csum2)
164 |
165 |
166 | def test_query_channel_range(runner: Runner) -> None:
167 | if runner.has_option("option_gossip_queries") is None:
168 | pytest.skip("Needs option_gossip_queries")
169 |
170 | funding1, funding1_tx = Funding.from_utxo(
171 | *utxo(0),
172 | local_node_privkey="02",
173 | local_funding_privkey="10",
174 | remote_node_privkey="03",
175 | remote_funding_privkey="20"
176 | )
177 |
178 | funding2, funding2_tx = Funding.from_utxo(
179 | *utxo(1),
180 | local_node_privkey="04",
181 | local_funding_privkey="30",
182 | remote_node_privkey="05",
183 | remote_funding_privkey="40"
184 | )
185 |
186 | timestamp_103x1x0_LOCAL = int(time.time())
187 | timestamp_109x1x0_LOCAL = timestamp_103x1x0_LOCAL - 1
188 | timestamp_109x1x0_REMOTE = timestamp_109x1x0_LOCAL - 1
189 |
190 | ts_103x1x0 = encode_timestamps(*funding1.node_id_sort(timestamp_103x1x0_LOCAL, 0))
191 | ts_109x1x0 = encode_timestamps(
192 | *funding2.node_id_sort(timestamp_109x1x0_LOCAL, timestamp_109x1x0_REMOTE)
193 | )
194 |
195 | update_103x1x0_LOCAL = funding1.channel_update(
196 | side=Side.local,
197 | short_channel_id="103x1x0",
198 | disable=False,
199 | cltv_expiry_delta=144,
200 | htlc_minimum_msat=0,
201 | fee_base_msat=1000,
202 | fee_proportional_millionths=10,
203 | timestamp=timestamp_103x1x0_LOCAL,
204 | htlc_maximum_msat=2000000,
205 | )
206 | update_109x1x0_LOCAL = funding2.channel_update(
207 | side=Side.local,
208 | short_channel_id="109x1x0",
209 | disable=False,
210 | cltv_expiry_delta=144,
211 | htlc_minimum_msat=0,
212 | fee_base_msat=1000,
213 | fee_proportional_millionths=10,
214 | timestamp=timestamp_109x1x0_LOCAL,
215 | htlc_maximum_msat=2000000,
216 | )
217 | update_109x1x0_REMOTE = funding2.channel_update(
218 | side=Side.remote,
219 | short_channel_id="109x1x0",
220 | disable=False,
221 | cltv_expiry_delta=144,
222 | htlc_minimum_msat=0,
223 | fee_base_msat=1000,
224 | fee_proportional_millionths=10,
225 | timestamp=timestamp_109x1x0_REMOTE,
226 | htlc_maximum_msat=2000000,
227 | )
228 |
229 | csums_103x1x0 = update_checksums(*funding1.node_id_sort(update_103x1x0_LOCAL, None))
230 | csums_109x1x0 = update_checksums(
231 | *funding2.node_id_sort(update_109x1x0_LOCAL, update_109x1x0_REMOTE)
232 | )
233 |
234 | test = [
235 | Block(blockheight=102, txs=[tx_spendable]),
236 | # Channel 103x1x0 (between 002 and 003)
237 | Block(blockheight=103, number=6, txs=[funding1_tx]),
238 | # Channel 109x1x0 (between 004 and 005)
239 | Block(blockheight=109, number=6, txs=[funding2_tx]),
240 | Connect(connprivkey="03"),
241 | ExpectMsg("init"),
242 | Msg(
243 | "init",
244 | globalfeatures=runner.runner_features(globals=True),
245 | features=runner.runner_features(),
246 | ),
247 | RawMsg(funding1.channel_announcement("103x1x0", "")),
248 | RawMsg(update_103x1x0_LOCAL),
249 | RawMsg(funding2.channel_announcement("109x1x0", "")),
250 | RawMsg(update_109x1x0_LOCAL),
251 | RawMsg(update_109x1x0_REMOTE),
252 | # New peer connects, with gossip_query option.
253 | Connect(connprivkey="05"),
254 | ExpectMsg("init"),
255 | # BOLT #9:
256 | # | 6/7 | `gossip_queries` | More sophisticated gossip control
257 | Msg(
258 | "init",
259 | globalfeatures=runner.runner_features(globals=True),
260 | features=runner.runner_features(additional_features=[7]),
261 | ),
262 | TryAll(
263 | # No queries? Must not get anything.
264 | [
265 | MustNotMsg("channel_announcement"),
266 | MustNotMsg("channel_update"),
267 | MustNotMsg("node_announcement"),
268 | ],
269 | # This should elicit an empty response
270 | [
271 | Msg(
272 | "query_channel_range",
273 | chain_hash=BitcoinUtils.blockchain_hash(),
274 | first_blocknum=0,
275 | number_of_blocks=103,
276 | ),
277 | ExpectMsg(
278 | "reply_channel_range",
279 | chain_hash=BitcoinUtils.blockchain_hash(),
280 | first_blocknum=0,
281 | number_of_blocks=103,
282 | ),
283 | CheckEq(decode_scids, ""),
284 | ],
285 | # This should get the first one, not the second.
286 | [
287 | Msg(
288 | "query_channel_range",
289 | chain_hash=BitcoinUtils.blockchain_hash(),
290 | first_blocknum=103,
291 | number_of_blocks=1,
292 | ),
293 | ExpectMsg(
294 | "reply_channel_range",
295 | chain_hash=BitcoinUtils.blockchain_hash(),
296 | first_blocknum=103,
297 | number_of_blocks=1,
298 | ),
299 | CheckEq(decode_scids, "103x1x0"),
300 | ],
301 | # This should get the second one, not the first.
302 | [
303 | Msg(
304 | "query_channel_range",
305 | chain_hash=BitcoinUtils.blockchain_hash(),
306 | first_blocknum=109,
307 | number_of_blocks=4294967295,
308 | ),
309 | OneOf(
310 | ExpectMsg(
311 | "reply_channel_range",
312 | chain_hash=BitcoinUtils.blockchain_hash(),
313 | first_blocknum=109,
314 | number_of_blocks=4294967186,
315 | ),
316 | # Could truncate number_of_blocks.
317 | ExpectMsg(
318 | "reply_channel_range",
319 | chain_hash=BitcoinUtils.blockchain_hash(),
320 | first_blocknum=109,
321 | number_of_blocks=1,
322 | ),
323 | ),
324 | CheckEq(decode_scids, "109x1x0"),
325 | ],
326 | # This should get both.
327 | [
328 | Msg(
329 | "query_channel_range",
330 | chain_hash=BitcoinUtils.blockchain_hash(),
331 | first_blocknum=103,
332 | number_of_blocks=7,
333 | ),
334 | ExpectMsg(
335 | "reply_channel_range",
336 | chain_hash=BitcoinUtils.blockchain_hash(),
337 | first_blocknum=103,
338 | number_of_blocks=7,
339 | ),
340 | CheckEq(decode_scids, "103x1x0,109x1x0"),
341 | ],
342 | # This should get appended timestamp fields with option_gossip_queries_ex
343 | Sequence(
344 | enable=runner.has_option("option_gossip_queries_ex") is not None,
345 | events=[
346 | Msg(
347 | "query_channel_range",
348 | chain_hash=BitcoinUtils.blockchain_hash(),
349 | first_blocknum=103,
350 | number_of_blocks=7,
351 | tlvs="{query_option={query_option_flags=1}}",
352 | ),
353 | ExpectMsg(
354 | "reply_channel_range",
355 | chain_hash=BitcoinUtils.blockchain_hash(),
356 | first_blocknum=103,
357 | number_of_blocks=7,
358 | ),
359 | CheckEq(decode_timestamps, ts_103x1x0 + ts_109x1x0),
360 | CheckEq(decode_scids, "103x1x0,109x1x0"),
361 | ],
362 | ),
363 | # This should get appended checksum fields with option_gossip_queries_ex
364 | Sequence(
365 | enable=runner.has_option("option_gossip_queries_ex") is not None,
366 | events=[
367 | Msg(
368 | "query_channel_range",
369 | chain_hash=BitcoinUtils.blockchain_hash(),
370 | first_blocknum=103,
371 | number_of_blocks=7,
372 | tlvs="{query_option={query_option_flags=2}}",
373 | ),
374 | ExpectMsg(
375 | "reply_channel_range",
376 | chain_hash=BitcoinUtils.blockchain_hash(),
377 | first_blocknum=103,
378 | number_of_blocks=7,
379 | tlvs="{checksums_tlv={checksums=["
380 | + csums_103x1x0
381 | + ","
382 | + csums_109x1x0
383 | + "]}}",
384 | ),
385 | CheckEq(decode_scids, "103x1x0,109x1x0"),
386 | ],
387 | ),
388 | # This should append timestamps and checksums with option_gossip_queries_ex
389 | Sequence(
390 | enable=runner.has_option("option_gossip_queries_ex") is not None,
391 | events=[
392 | Msg(
393 | "query_channel_range",
394 | chain_hash=BitcoinUtils.blockchain_hash(),
395 | first_blocknum=103,
396 | number_of_blocks=7,
397 | tlvs="{query_option={query_option_flags=3}}",
398 | ),
399 | ExpectMsg(
400 | "reply_channel_range",
401 | chain_hash=BitcoinUtils.blockchain_hash(),
402 | first_blocknum=103,
403 | number_of_blocks=7,
404 | tlvs="{checksums_tlv={checksums=["
405 | + csums_103x1x0
406 | + ","
407 | + csums_109x1x0
408 | + "]}}",
409 | ),
410 | CheckEq(decode_timestamps, ts_103x1x0 + ts_109x1x0),
411 | CheckEq(decode_scids, "103x1x0,109x1x0"),
412 | ],
413 | ),
414 | ),
415 | ]
416 |
417 | runner.run(test)
418 |
--------------------------------------------------------------------------------
/tools/check_quotes.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/python3
2 | import fileinput
3 | import glob
4 | import re
5 | import sys
6 | from argparse import ArgumentParser, REMAINDER, Namespace
7 | from collections import namedtuple
8 | from typing import Dict, List, Tuple, Optional
9 |
10 | Quote = namedtuple("Quote", ["filename", "line", "text"])
11 | whitespace_re = re.compile(r"\s+")
12 |
13 |
14 | def collapse_whitespace(string: str) -> str:
15 | return whitespace_re.sub(" ", string)
16 |
17 |
18 | def add_quote(
19 | boltquotes: Dict[int, List[Quote]],
20 | boltnum: int,
21 | filename: str,
22 | line: int,
23 | quote: str,
24 | ) -> None:
25 | if boltnum not in boltquotes:
26 | boltquotes[boltnum] = []
27 | boltquotes[boltnum].append(
28 | Quote(filename, line, collapse_whitespace(quote.strip()))
29 | )
30 |
31 |
32 | def included_commit(args: Namespace, boltprefix: str) -> bool:
33 | for inc in args.include_commit:
34 | if boltprefix.startswith(inc):
35 | return True
36 | return False
37 |
38 |
39 | # This looks like a BOLT line; return the bolt number and start of
40 | # quote if we shouldn't ignore it.
41 | def get_boltstart(
42 | args: Namespace, line: str, filename: str, linenum: int
43 | ) -> Tuple[Optional[int], Optional[str]]:
44 | if not line.startswith(args.comment_start + "BOLT"):
45 | return None, None
46 |
47 | parts = line[len(args.comment_start + "BOLT") :].partition(":")
48 | boltnum = parts[0].strip()
49 |
50 | # e.g. BOLT-50143e388e16a449a92ed574fc16eb35b51426b9 #11:"
51 | if boltnum.startswith("-"):
52 | if not included_commit(args, boltnum[1:]):
53 | return None, None
54 | boltnum = boltnum.partition(" ")[2]
55 |
56 | if not boltnum.startswith("#"):
57 | print(
58 | "{}:{}:expected # after BOLT in {}".format(filename, linenum, line),
59 | file=sys.stderr,
60 | )
61 | sys.exit(1)
62 |
63 | try:
64 | boltint = int(boltnum[1:].strip())
65 | except ValueError:
66 | print(
67 | "{}:{}:bad bolt number {}".format(filename, linenum, line), file=sys.stderr
68 | )
69 | sys.exit(1)
70 |
71 | return boltint, parts[2]
72 |
73 |
74 | # We expect lines to start with '# BOLT #NN:'
75 | def gather_quotes(args: Namespace) -> Dict[int, List[Quote]]:
76 | boltquotes: Dict[int, List[Quote]] = {}
77 | curquote = None
78 | # These initializations simply keep flake8 happy
79 | curbolt = 0
80 | filestart = ""
81 | linestart = 0
82 | for file_line in fileinput.input(args.files):
83 | line = file_line.strip()
84 | boltnum, quote = get_boltstart(
85 | args, line, fileinput.filename(), fileinput.filelineno()
86 | )
87 | if boltnum is not None:
88 | if curquote is not None:
89 | add_quote(boltquotes, curbolt, filestart, linestart, curquote)
90 |
91 | linestart = fileinput.filelineno()
92 | filestart = fileinput.filename()
93 | curbolt = boltnum
94 | curquote = quote
95 | elif curquote is not None:
96 | # If this is a continuation (and not an end!), add it.
97 | if (
98 | args.comment_end is None or not line.startswith(args.comment_end)
99 | ) and line.startswith(args.comment_continue):
100 | # Special case where end marker is on same line.
101 | if args.comment_end is not None and line.endswith(args.comment_end):
102 | curquote += (
103 | " " + line[len(args.comment_continue) : -len(args.comment_end)]
104 | )
105 | add_quote(boltquotes, curbolt, filestart, linestart, curquote)
106 | curquote = None
107 | else:
108 | curquote += " " + line[len(args.comment_continue) :]
109 | else:
110 | add_quote(boltquotes, curbolt, filestart, linestart, curquote)
111 | curquote = None
112 |
113 | # Handle quote at eof.
114 | if curquote is not None:
115 | add_quote(boltquotes, curbolt, filestart, linestart, curquote)
116 |
117 | return boltquotes
118 |
119 |
120 | def load_bolt(boltdir: str, num: int) -> List[str]:
121 | """Return a list, divided into one-string-per-bolt-section, with
122 | whitespace collapsed into single spaces.
123 |
124 | """
125 | boltfile = glob.glob("{}/{}-*md".format(boltdir, str(num).zfill(2)))
126 | if len(boltfile) == 0:
127 | print("Cannot find bolt {} in {}".format(num, boltdir), file=sys.stderr)
128 | sys.exit(1)
129 | elif len(boltfile) > 1:
130 | print(
131 | "More than one bolt {} in {}? {}".format(num, boltdir, boltfile),
132 | file=sys.stderr,
133 | )
134 | sys.exit(1)
135 |
136 | # We divide it into sections, and collapse whitespace.
137 | boltsections = []
138 | with open(boltfile[0]) as f:
139 | sect = ""
140 | for line in f.readlines():
141 | if line.startswith("#"):
142 | # Append with whitespace collapsed.
143 | boltsections.append(collapse_whitespace(sect))
144 | sect = ""
145 | sect += line
146 | boltsections.append(collapse_whitespace(sect))
147 |
148 | return boltsections
149 |
150 |
151 | def find_quote(
152 | text: str, boltsections: List[str]
153 | ) -> Tuple[Optional[str], Optional[int]]:
154 | # '...' means "match anything".
155 | textparts = text.split("...")
156 | for b in boltsections:
157 | off = 0
158 | for part in textparts:
159 | off = b.find(part, off)
160 | if off == -1:
161 | break
162 | if off != -1:
163 | return b, off + len(part)
164 | return None, None
165 |
166 |
167 | def main(args: Namespace) -> None:
168 | boltquotes = gather_quotes(args)
169 | for bolt in boltquotes:
170 | boltsections = load_bolt(args.boltdir, bolt)
171 | for quote in boltquotes[bolt]:
172 | sect, end = find_quote(quote.text, boltsections)
173 | if not sect:
174 | print(
175 | "{}:{}:cannot find match".format(quote.filename, quote.line),
176 | file=sys.stderr,
177 | )
178 | # Reduce the text until we find a match.
179 | for n in range(len(quote.text), -1, -1):
180 | sect, end = find_quote(quote.text[:n], boltsections)
181 | if sect:
182 | print(
183 | " common prefix: {}...".format(quote.text[:n]),
184 | file=sys.stderr,
185 | )
186 | print(
187 | " expected ...{:.45}".format(sect[end:]), file=sys.stderr
188 | )
189 | print(
190 | " but have ...{:.45}".format(quote.text[n:]),
191 | file=sys.stderr,
192 | )
193 | break
194 | sys.exit(1)
195 | elif args.verbose:
196 | print(
197 | "{}:{}:Matched {} in {}".format(
198 | quote.filename, quote.line, quote.text, sect
199 | )
200 | )
201 |
202 |
203 | if __name__ == "__main__":
204 | parser = ArgumentParser(
205 | description="Check BOLT quotes in the given files are correct"
206 | )
207 | parser.add_argument("-v", "--verbose", action="store_true")
208 | # e.g. for C code these are '/* ', '*' and '*/'
209 | parser.add_argument(
210 | "--comment-start", help='marker for start of "BOLT #N" quote', default="# "
211 | )
212 | parser.add_argument(
213 | "--comment-continue", help='marker for continued "BOLT #N" quote', default="#"
214 | )
215 | parser.add_argument("--comment-end", help='marker for end of "BOLT #N" quote')
216 | parser.add_argument(
217 | "--include-commit",
218 | action="append",
219 | help="Also parse BOLT- quotes",
220 | default=[],
221 | )
222 | parser.add_argument(
223 | "--boltdir", help="Directory to look for BOLT tests", default="../lightning-rfc"
224 | )
225 | parser.add_argument("files", help="Files to read in (or stdin)", nargs=REMAINDER)
226 |
227 | args = parser.parse_args()
228 | main(args)
229 |
--------------------------------------------------------------------------------